this repo has no description
0
fork

Configure Feed

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

Breaking: refactor all masto API calls

Everything need to be instance-aware!

+481 -253
+32 -98
src/app.jsx
··· 2 2 import 'toastify-js/src/toastify.css'; 3 3 4 4 import debounce from 'just-debounce-it'; 5 - import { createClient } from 'masto'; 6 5 import { 7 6 useEffect, 8 7 useLayoutEffect, ··· 36 35 import Settings from './pages/settings'; 37 36 import Status from './pages/status'; 38 37 import Welcome from './pages/welcome'; 38 + import { api, initAccount, initClient, initInstance } from './utils/api'; 39 39 import { getAccessToken } from './utils/auth'; 40 40 import states, { saveStatus } from './utils/states'; 41 41 import store from './utils/store'; 42 + import { getCurrentAccount } from './utils/store-utils'; 42 43 43 44 window.__STATES__ = states; 44 45 ··· 54 55 document.documentElement.classList.add(`is-${theme}`); 55 56 document 56 57 .querySelector('meta[name="color-scheme"]') 57 - .setAttribute('content', theme); 58 + .setAttribute('content', theme === 'auto' ? 'dark light' : theme); 58 59 } 59 60 }, []); 60 61 61 62 useEffect(() => { 62 63 const instanceURL = store.local.get('instanceURL'); 63 - const accounts = store.local.getJSON('accounts') || []; 64 64 const code = (window.location.search.match(/code=([^&]+)/) || [])[1]; 65 65 66 66 if (code) { ··· 73 73 74 74 (async () => { 75 75 setUIState('loading'); 76 - const tokenJSON = await getAccessToken({ 76 + const { access_token: accessToken } = await getAccessToken({ 77 77 instanceURL, 78 78 client_id: clientID, 79 79 client_secret: clientSecret, 80 80 code, 81 81 }); 82 - const { access_token: accessToken } = tokenJSON; 83 - store.session.set('accessToken', accessToken); 84 82 85 - initMasto({ 86 - url: `https://${instanceURL}`, 87 - accessToken, 88 - }); 89 - 90 - const mastoAccount = await masto.v1.accounts.verifyCredentials(); 91 - 92 - // console.log({ tokenJSON, mastoAccount }); 93 - 94 - let account = accounts.find((a) => a.info.id === mastoAccount.id); 95 - if (account) { 96 - account.info = mastoAccount; 97 - account.instanceURL = instanceURL.toLowerCase(); 98 - account.accessToken = accessToken; 99 - } else { 100 - account = { 101 - info: mastoAccount, 102 - instanceURL, 103 - accessToken, 104 - }; 105 - accounts.push(account); 106 - } 107 - 108 - store.local.setJSON('accounts', accounts); 109 - store.session.set('currentAccount', account.info.id); 83 + const masto = initClient({ instance: instanceURL, accessToken }); 84 + await Promise.allSettled([ 85 + initInstance(masto), 86 + initAccount(masto, instanceURL, accessToken), 87 + ]); 110 88 111 89 setIsLoggedIn(true); 112 90 setUIState('default'); 113 91 })(); 114 - } else if (accounts.length) { 115 - const currentAccount = store.session.get('currentAccount'); 116 - const account = 117 - accounts.find((a) => a.info.id === currentAccount) || accounts[0]; 118 - const instanceURL = account.instanceURL; 119 - const accessToken = account.accessToken; 120 - store.session.set('currentAccount', account.info.id); 121 - if (accessToken) setIsLoggedIn(true); 122 - 123 - initMasto({ 124 - url: `https://${instanceURL}`, 125 - accessToken, 126 - }); 127 92 } else { 93 + const account = getCurrentAccount(); 94 + if (account) { 95 + store.session.set('currentAccount', account.info.id); 96 + const { masto } = api({ account }); 97 + initInstance(masto); 98 + setIsLoggedIn(true); 99 + } 100 + 128 101 setUIState('default'); 129 102 } 130 103 }, []); ··· 181 154 182 155 const nonRootLocation = useMemo(() => { 183 156 const { pathname } = location; 184 - return !/^\/(login|welcome|p)/.test(pathname); 157 + return !/^\/(login|welcome)/.test(pathname); 185 158 }, [location]); 159 + 160 + console.log('nonRootLocation', nonRootLocation, 'location', location); 186 161 187 162 return ( 188 163 <> ··· 210 185 {isLoggedIn && <Route path="/b" element={<Bookmarks />} />} 211 186 {isLoggedIn && <Route path="/f" element={<Favourites />} />} 212 187 {isLoggedIn && <Route path="/l/:id" element={<Lists />} />} 213 - {isLoggedIn && <Route path="/t/:hashtag" element={<Hashtags />} />} 214 - {isLoggedIn && <Route path="/a/:id" element={<AccountStatuses />} />} 188 + {isLoggedIn && ( 189 + <Route path="/t/:instance?/:hashtag" element={<Hashtags />} /> 190 + )} 191 + {isLoggedIn && ( 192 + <Route path="/a/:instance?/:id" element={<AccountStatuses />} /> 193 + )} 215 194 <Route path="/p/l?/:instance" element={<Public />} /> 216 195 {/* <Route path="/:anything" element={<NotFound />} /> */} 217 196 </Routes> 218 197 <Routes> 219 - <Route path="/s/:id" element={<Status />} /> 198 + <Route path="/s/:instance?/:id" element={<Status />} /> 220 199 </Routes> 221 200 <nav id="tab-bar" hidden> 222 201 <li> ··· 304 283 }} 305 284 > 306 285 <Account 307 - account={snapStates.showAccount} 286 + account={snapStates.showAccount?.account || snapStates.showAccount} 287 + instance={snapStates.showAccount?.instance} 308 288 onClose={() => { 309 289 states.showAccount = false; 310 290 }} ··· 335 315 > 336 316 <MediaModal 337 317 mediaAttachments={snapStates.showMediaModal.mediaAttachments} 318 + instance={snapStates.showMediaModal.instance} 338 319 index={snapStates.showMediaModal.index} 339 320 statusID={snapStates.showMediaModal.statusID} 340 321 onClose={() => { ··· 347 328 ); 348 329 } 349 330 350 - function initMasto(params) { 351 - const clientParams = { 352 - url: params.url || 'https://mastodon.social', 353 - accessToken: params.accessToken || null, 354 - disableVersionCheck: true, 355 - timeout: 30_000, 356 - }; 357 - window.masto = createClient(clientParams); 358 - 359 - (async () => { 360 - // Request v2, fallback to v1 if fail 361 - let info; 362 - try { 363 - info = await masto.v2.instance.fetch(); 364 - } catch (e) {} 365 - if (!info) { 366 - try { 367 - info = await masto.v1.instances.fetch(); 368 - } catch (e) {} 369 - } 370 - if (!info) return; 371 - console.log(info); 372 - const { 373 - // v1 374 - uri, 375 - urls: { streamingApi } = {}, 376 - // v2 377 - domain, 378 - configuration: { urls: { streaming } = {} } = {}, 379 - } = info; 380 - if (uri || domain) { 381 - const instances = store.local.getJSON('instances') || {}; 382 - instances[ 383 - (domain || uri) 384 - .replace(/^https?:\/\//, '') 385 - .replace(/\/+$/, '') 386 - .toLowerCase() 387 - ] = info; 388 - store.local.setJSON('instances', instances); 389 - } 390 - if (streamingApi || streaming) { 391 - window.masto = createClient({ 392 - ...clientParams, 393 - streamingApiUrl: streaming || streamingApi, 394 - }); 395 - } 396 - })(); 397 - } 398 - 399 331 let ws; 400 332 async function startStream() { 333 + const { masto } = api(); 401 334 if ( 402 335 ws && 403 336 (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN) ··· 472 405 473 406 let lastHidden; 474 407 function startVisibility() { 408 + const { masto } = api(); 475 409 const handleVisible = (visible) => { 476 410 if (!visible) { 477 411 const timestamp = Date.now();
+13 -6
src/components/account.jsx
··· 2 2 3 3 import { useEffect, useState } from 'preact/hooks'; 4 4 5 + import { api } from '../utils/api'; 5 6 import emojifyText from '../utils/emojify-text'; 6 7 import enhanceContent from '../utils/enhance-content'; 7 8 import handleContentLinks from '../utils/handle-content-links'; ··· 14 15 import Link from './link'; 15 16 import NameText from './name-text'; 16 17 17 - function Account({ account, onClose }) { 18 + function Account({ account, instance, onClose }) { 19 + const { masto, authenticated } = api({ instance }); 18 20 const [uiState, setUIState] = useState('default'); 19 21 const isString = typeof account === 'string'; 20 22 const [info, setInfo] = useState(isString ? null : account); ··· 82 84 const [relationship, setRelationship] = useState(null); 83 85 const [familiarFollowers, setFamiliarFollowers] = useState([]); 84 86 useEffect(() => { 85 - if (info) { 87 + if (info && authenticated) { 86 88 const currentAccount = store.session.get('currentAccount'); 87 89 if (currentAccount === id) { 88 90 // It's myself! ··· 120 122 } 121 123 })(); 122 124 } 123 - }, [info]); 125 + }, [info, authenticated]); 124 126 125 127 const { 126 128 following, ··· 174 176 <> 175 177 <header> 176 178 <Avatar url={avatar} size="xxxl" /> 177 - <NameText account={info} showAcct external /> 179 + <NameText account={info} instance={instance} showAcct external /> 178 180 </header> 179 181 <main tabIndex="-1"> 180 182 {bot && ( ··· 186 188 )} 187 189 <div 188 190 class="note" 189 - onClick={handleContentLinks()} 191 + onClick={handleContentLinks({ 192 + instance, 193 + })} 190 194 dangerouslySetInnerHTML={{ 191 195 __html: enhanceContent(note, { emojis }), 192 196 }} ··· 270 274 rel="noopener noreferrer" 271 275 onClick={(e) => { 272 276 e.preventDefault(); 273 - states.showAccount = follower; 277 + states.showAccount = { 278 + account: follower, 279 + instance, 280 + }; 274 281 }} 275 282 > 276 283 <Avatar
+7 -2
src/components/compose.jsx
··· 12 12 13 13 import supportedLanguages from '../data/status-supported-languages'; 14 14 import urlRegex from '../data/url-regex'; 15 + import { api } from '../utils/api'; 15 16 import db from '../utils/db'; 16 17 import emojifyText from '../utils/emojify-text'; 17 18 import openCompose from '../utils/open-compose'; ··· 99 100 hasOpener, 100 101 }) { 101 102 console.warn('RENDER COMPOSER'); 103 + const { masto } = api(); 102 104 const [uiState, setUIState] = useState('default'); 103 105 const UID = useRef(draftStatus?.uid || uid()); 104 106 console.log('Compose UID', UID.current); ··· 868 870 updateCharCount(); 869 871 }} 870 872 maxCharacters={maxCharacters} 873 + performSearch={(params) => { 874 + return masto.v2.search(params); 875 + }} 871 876 /> 872 877 {mediaAttachments.length > 0 && ( 873 878 <div class="media-attachments"> ··· 1031 1036 1032 1037 const Textarea = forwardRef((props, ref) => { 1033 1038 const [text, setText] = useState(ref.current?.value || ''); 1034 - const { maxCharacters, ...textareaProps } = props; 1039 + const { maxCharacters, performSearch = () => {}, ...textareaProps } = props; 1035 1040 const snapStates = useSnapshot(states); 1036 1041 const charCount = snapStates.composerCharacterCount; 1037 1042 ··· 1087 1092 }[key]; 1088 1093 provide( 1089 1094 new Promise((resolve) => { 1090 - const searchResults = masto.v2.search({ 1095 + const searchResults = performSearch({ 1091 1096 type, 1092 1097 q: text, 1093 1098 limit: 5,
+2
src/components/drafts.jsx
··· 2 2 3 3 import { useEffect, useMemo, useReducer, useState } from 'react'; 4 4 5 + import { api } from '../utils/api'; 5 6 import db from '../utils/db'; 6 7 import states from '../utils/states'; 7 8 import { getCurrentAccountNS } from '../utils/store-utils'; ··· 10 11 import Loader from './loader'; 11 12 12 13 function Drafts() { 14 + const { masto } = api(); 13 15 const [uiState, setUIState] = useState('default'); 14 16 const [drafts, setDrafts] = useState([]); 15 17 const [reloadCount, reload] = useReducer((c) => c + 1, 0);
+3 -2
src/components/media-modal.jsx
··· 11 11 function MediaModal({ 12 12 mediaAttachments, 13 13 statusID, 14 + instance, 14 15 index = 0, 15 16 onClose = () => {}, 16 17 }) { 17 18 const carouselRef = useRef(null); 18 - const isStatusLocation = useMatch('/s/:id'); 19 + const isStatusLocation = useMatch('/s/:instance?/:id'); 19 20 20 21 const [currentIndex, setCurrentIndex] = useState(index); 21 22 const carouselFocusItem = useRef(null); ··· 167 168 <span> 168 169 {!isStatusLocation && ( 169 170 <Link 170 - to={`/s/${statusID}`} 171 + to={instance ? `/s/${instance}/${statusID}` : `/s/${statusID}`} 171 172 class="button carousel-button media-post-link plain3" 172 173 onClick={() => { 173 174 // if small screen (not media query min-width 40em + 350px), run onClose
+13 -2
src/components/name-text.jsx
··· 5 5 6 6 import Avatar from './avatar'; 7 7 8 - function NameText({ account, showAvatar, showAcct, short, external, onClick }) { 8 + function NameText({ 9 + account, 10 + instance, 11 + showAvatar, 12 + showAcct, 13 + short, 14 + external, 15 + onClick, 16 + }) { 9 17 const { acct, avatar, avatarStatic, id, url, displayName, emojis } = account; 10 18 let { username } = account; 11 19 ··· 34 42 if (external) return; 35 43 e.preventDefault(); 36 44 if (onClick) return onClick(e); 37 - states.showAccount = account; 45 + states.showAccount = { 46 + account, 47 + instance, 48 + }; 38 49 }} 39 50 > 40 51 {showAvatar && (
+95 -42
src/components/status.jsx
··· 1 1 import './status.css'; 2 2 3 3 import { Menu, MenuItem } from '@szhsin/react-menu'; 4 - import { getBlurHashAverageColor } from 'fast-blurhash'; 5 4 import mem from 'mem'; 6 5 import { memo } from 'preact/compat'; 7 - import { 8 - useEffect, 9 - useLayoutEffect, 10 - useMemo, 11 - useRef, 12 - useState, 13 - } from 'preact/hooks'; 14 - import { useHotkeys } from 'react-hotkeys-hook'; 6 + import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; 15 7 import 'swiped-events'; 16 8 import useResizeObserver from 'use-resize-observer'; 17 9 import { useSnapshot } from 'valtio'; ··· 19 11 import Loader from '../components/loader'; 20 12 import Modal from '../components/modal'; 21 13 import NameText from '../components/name-text'; 14 + import { api } from '../utils/api'; 22 15 import enhanceContent from '../utils/enhance-content'; 23 16 import handleContentLinks from '../utils/handle-content-links'; 24 17 import htmlContentLength from '../utils/html-content-length'; ··· 33 26 import Media from './media'; 34 27 import RelativeTime from './relative-time'; 35 28 36 - function fetchAccount(id) { 29 + function fetchAccount(id, masto) { 37 30 try { 38 31 return masto.v1.accounts.fetch(id); 39 32 } catch (e) { ··· 45 38 function Status({ 46 39 statusID, 47 40 status, 41 + instance, 48 42 withinContext, 49 43 size = 'm', 50 44 skeleton, ··· 65 59 </div> 66 60 ); 67 61 } 62 + const { masto, authenticated } = api({ instance }); 68 63 69 64 const snapStates = useSnapshot(states); 70 65 if (!status) { ··· 135 130 if (account) { 136 131 setInReplyToAccount(account); 137 132 } else { 138 - memFetchAccount(inReplyToAccountId) 133 + memFetchAccount(inReplyToAccountId, masto) 139 134 .then((account) => { 140 135 setInReplyToAccount(account); 141 136 states.accounts[account.id] = account; ··· 157 152 <div class="status-reblog" onMouseEnter={debugHover}> 158 153 <div class="status-pre-meta"> 159 154 <Icon icon="rocket" size="l" />{' '} 160 - <NameText account={status.account} showAvatar /> boosted 155 + <NameText account={status.account} instance={instance} showAvatar />{' '} 156 + boosted 161 157 </div> 162 - <Status status={reblog} size={size} /> 158 + <Status status={reblog} instance={instance} size={size} /> 163 159 </div> 164 160 ); 165 161 } ··· 198 194 199 195 const statusRef = useRef(null); 200 196 197 + const unauthInteractionErrorMessage = `Sorry, your current logged-in instance can't interact with this status from another instance.`; 198 + 201 199 return ( 202 200 <article 203 201 ref={statusRef} ··· 229 227 onClick={(e) => { 230 228 e.preventDefault(); 231 229 e.stopPropagation(); 232 - states.showAccount = status.account; 230 + states.showAccount = { 231 + account: status.account, 232 + instance, 233 + }; 233 234 }} 234 235 > 235 236 <Avatar url={avatarStatic} size="xxl" /> ··· 240 241 {/* <span> */} 241 242 <NameText 242 243 account={status.account} 244 + instance={instance} 243 245 showAvatar={size === 's'} 244 246 showAcct={size === 'l'} 245 247 /> ··· 248 250 {' '} 249 251 <span class="ib"> 250 252 <Icon icon="arrow-right" class="arrow" />{' '} 251 - <NameText account={inReplyToAccount} short /> 253 + <NameText account={inReplyToAccount} instance={instance} short /> 252 254 </span> 253 255 </> 254 256 )} */} 255 257 {/* </span> */}{' '} 256 258 {size !== 'l' && 257 259 (uri ? ( 258 - <Link to={`/s/${id}`} class="time"> 260 + <Link 261 + to={ 262 + instance 263 + ? ` 264 + /s/${instance}/${id} 265 + ` 266 + : `/s/${id}` 267 + } 268 + class="time" 269 + > 259 270 <Icon 260 271 icon={visibilityIconsMap[visibility]} 261 272 alt={visibility} ··· 294 305 })) && ( 295 306 <div class="status-reply-badge"> 296 307 <Icon icon="reply" />{' '} 297 - <NameText account={inReplyToAccount} short /> 308 + <NameText 309 + account={inReplyToAccount} 310 + instance={instance} 311 + short 312 + /> 298 313 </div> 299 314 ) 300 315 )} ··· 346 361 lang={language} 347 362 ref={contentRef} 348 363 data-read-more={readMoreText} 349 - onClick={handleContentLinks({ mentions })} 364 + onClick={handleContentLinks({ mentions, instance })} 350 365 dangerouslySetInnerHTML={{ 351 366 __html: enhanceContent(content, { 352 367 emojis, ··· 367 382 <Poll 368 383 lang={language} 369 384 poll={poll} 370 - readOnly={readOnly} 385 + readOnly={readOnly || !authenticated} 371 386 onUpdate={(newPoll) => { 372 387 states.statuses[id].poll = newPoll; 373 388 }} 389 + refresh={() => { 390 + return masto.v1.polls 391 + .fetch(poll.id) 392 + .then((pollResponse) => { 393 + states.statuses[id].poll = pollResponse; 394 + }) 395 + .catch((e) => {}); // Silently fail 396 + }} 397 + votePoll={(choices) => { 398 + return masto.v1.polls 399 + .vote(poll.id, { 400 + choices, 401 + }) 402 + .then((pollResponse) => { 403 + states.statuses[id].poll = pollResponse; 404 + }) 405 + .catch((e) => {}); // Silently fail 406 + }} 374 407 /> 375 408 )} 376 409 {!spoilerText && sensitive && !!mediaAttachments.length && ( ··· 410 443 states.showMediaModal = { 411 444 mediaAttachments, 412 445 index: i, 446 + instance, 413 447 statusID: readOnly ? null : id, 414 448 }; 415 449 }} ··· 477 511 icon="comment" 478 512 count={repliesCount} 479 513 onClick={() => { 514 + if (!authenticated) { 515 + return alert(unauthInteractionErrorMessage); 516 + } 480 517 states.showCompose = { 481 518 replyToStatus: status, 482 519 }; ··· 494 531 icon="rocket" 495 532 count={reblogsCount} 496 533 onClick={async () => { 534 + if (!authenticated) { 535 + return alert(unauthInteractionErrorMessage); 536 + } 497 537 try { 498 538 if (!reblogged) { 499 539 const yes = confirm( ··· 536 576 icon="heart" 537 577 count={favouritesCount} 538 578 onClick={async () => { 579 + if (!authenticated) { 580 + return alert(unauthInteractionErrorMessage); 581 + } 539 582 try { 540 583 // Optimistic 541 584 states.statuses[statusID] = { ··· 569 612 class="bookmark-button" 570 613 icon="bookmark" 571 614 onClick={async () => { 615 + if (!authenticated) { 616 + return alert(unauthInteractionErrorMessage); 617 + } 572 618 try { 573 619 // Optimistic 574 620 states.statuses[statusID] = { ··· 635 681 > 636 682 <EditedAtModal 637 683 statusID={showEdited} 684 + instance={instance} 685 + fetchStatusHistory={() => { 686 + return masto.v1.statuses.listHistory(showEdited); 687 + }} 638 688 onClose={() => { 639 689 setShowEdited(false); 640 690 statusRef.current?.focus(); ··· 742 792 } 743 793 } 744 794 745 - function Poll({ poll, lang, readOnly, onUpdate = () => {} }) { 795 + function Poll({ 796 + poll, 797 + lang, 798 + readOnly, 799 + refresh = () => {}, 800 + votePoll = () => {}, 801 + }) { 746 802 const [uiState, setUIState] = useState('default'); 747 803 748 804 const { ··· 768 824 timeout = setTimeout(() => { 769 825 setUIState('loading'); 770 826 (async () => { 771 - try { 772 - const pollResponse = await masto.v1.polls.fetch(id); 773 - onUpdate(pollResponse); 774 - } catch (e) { 775 - // Silent fail 776 - } 827 + await refresh(); 777 828 setUIState('default'); 778 829 })(); 779 830 }, ms); ··· 847 898 e.preventDefault(); 848 899 const form = e.target; 849 900 const formData = new FormData(form); 850 - const votes = []; 901 + const choices = []; 851 902 formData.forEach((value, key) => { 852 903 if (key === 'poll') { 853 - votes.push(value); 904 + choices.push(value); 854 905 } 855 906 }); 856 907 console.log(votes); 857 908 setUIState('loading'); 858 - const pollResponse = await masto.v1.polls.vote(id, { 859 - choices: votes, 860 - }); 861 - console.log(pollResponse); 862 - onUpdate(pollResponse); 909 + await votePoll(choices); 863 910 setUIState('default'); 864 911 }} 865 912 > ··· 903 950 e.preventDefault(); 904 951 setUIState('loading'); 905 952 (async () => { 906 - try { 907 - const pollResponse = await masto.v1.polls.fetch(id); 908 - onUpdate(pollResponse); 909 - } catch (e) { 910 - // Silent fail 911 - } 953 + await refresh(); 912 954 setUIState('default'); 913 955 })(); 914 956 }} ··· 937 979 ); 938 980 } 939 981 940 - function EditedAtModal({ statusID, onClose = () => {} }) { 982 + function EditedAtModal({ 983 + statusID, 984 + instance, 985 + fetchStatusHistory = () => {}, 986 + onClose = () => {}, 987 + }) { 941 988 const [uiState, setUIState] = useState('default'); 942 989 const [editHistory, setEditHistory] = useState([]); 943 990 ··· 945 992 setUIState('loading'); 946 993 (async () => { 947 994 try { 948 - const editHistory = await masto.v1.statuses.listHistory(statusID); 995 + const editHistory = await fetchStatusHistory(); 949 996 console.log(editHistory); 950 997 setEditHistory(editHistory); 951 998 setUIState('default'); ··· 997 1044 }).format(createdAtDate)} 998 1045 </time> 999 1046 </h3> 1000 - <Status status={status} size="s" withinContext readOnly /> 1047 + <Status 1048 + status={status} 1049 + instance={instance} 1050 + size="s" 1051 + withinContext 1052 + readOnly 1053 + /> 1001 1054 </li> 1002 1055 ); 1003 1056 })}
+13 -6
src/components/timeline.jsx
··· 12 12 title, 13 13 titleComponent, 14 14 id, 15 + instance, 15 16 emptyText, 16 17 errorText, 17 18 boostsCarousel, ··· 112 113 {items.map((status) => { 113 114 const { id: statusID, reblog, boosts } = status; 114 115 const actualStatusID = reblog?.id || statusID; 116 + const url = instance 117 + ? `/s/${instance}/${actualStatusID}` 118 + : `/s/${actualStatusID}`; 115 119 if (boosts) { 116 120 return ( 117 121 <li key={`timeline-${statusID}`}> 118 - <BoostsCarousel boosts={boosts} /> 122 + <BoostsCarousel boosts={boosts} instance={instance} /> 119 123 </li> 120 124 ); 121 125 } 122 126 return ( 123 127 <li key={`timeline-${statusID}`}> 124 - <Link class="status-link" to={`/s/${actualStatusID}`}> 125 - <Status status={status} /> 128 + <Link class="status-link" to={url}> 129 + <Status status={status} instance={instance} /> 126 130 </Link> 127 131 </li> 128 132 ); ··· 213 217 } 214 218 } 215 219 216 - function BoostsCarousel({ boosts }) { 220 + function BoostsCarousel({ boosts, instance }) { 217 221 const carouselRef = useRef(); 218 222 const { reachStart, reachEnd, init } = useScroll({ 219 223 scrollableElement: carouselRef.current, ··· 260 264 {boosts.map((boost) => { 261 265 const { id: statusID, reblog } = boost; 262 266 const actualStatusID = reblog?.id || statusID; 267 + const url = instance 268 + ? `/s/${instance}/${actualStatusID}` 269 + : `/s/${actualStatusID}`; 263 270 return ( 264 271 <li key={statusID}> 265 - <Link class="status-boost-link" to={`/s/${actualStatusID}`}> 266 - <Status status={boost} size="s" /> 272 + <Link class="status-boost-link" to={url}> 273 + <Status status={boost} instance={instance} size="s" /> 267 274 </Link> 268 275 </li> 269 276 );
-20
src/compose.jsx
··· 2 2 3 3 import './app.css'; 4 4 5 - import { createClient } from 'masto'; 6 5 import { render } from 'preact'; 7 6 import { useEffect, useState } from 'preact/hooks'; 8 7 9 8 import Compose from './components/compose'; 10 - import { getCurrentAccount } from './utils/store-utils'; 11 9 import useTitle from './utils/useTitle'; 12 10 13 11 if (window.opener) { 14 12 console = window.opener.console; 15 13 } 16 - 17 - (() => { 18 - if (window.masto) return; 19 - console.warn('window.masto not found. Trying to log in...'); 20 - try { 21 - const { instanceURL, accessToken } = getCurrentAccount(); 22 - window.masto = createClient({ 23 - url: `https://${instanceURL}`, 24 - accessToken, 25 - disableVersionCheck: true, 26 - timeout: 30_000, 27 - }); 28 - console.info('Logged in successfully.'); 29 - } catch (e) { 30 - console.error(e); 31 - alert('Failed to log in. Please try again.'); 32 - } 33 - })(); 34 14 35 15 function App() { 36 16 const [uiState, setUIState] = useState('default');
+7 -2
src/pages/account-statuses.jsx
··· 3 3 import { useSnapshot } from 'valtio'; 4 4 5 5 import Timeline from '../components/timeline'; 6 + import { api } from '../utils/api'; 6 7 import emojifyText from '../utils/emojify-text'; 7 8 import states from '../utils/states'; 8 9 import useTitle from '../utils/useTitle'; ··· 11 12 12 13 function AccountStatuses() { 13 14 const snapStates = useSnapshot(states); 14 - const { id } = useParams(); 15 + const { id, instance } = useParams(); 16 + const { masto } = api({ instance }); 15 17 const accountStatusesIterator = useRef(); 16 18 async function fetchAccountStatuses(firstLoad) { 17 19 if (firstLoad || !accountStatusesIterator.current) { ··· 46 48 <h1 47 49 class="header-account" 48 50 onClick={() => { 49 - states.showAccount = account; 51 + states.showAccount = { 52 + account, 53 + instance, 54 + }; 50 55 }} 51 56 > 52 57 <b
+2
src/pages/bookmarks.jsx
··· 1 1 import { useRef } from 'preact/hooks'; 2 2 3 3 import Timeline from '../components/timeline'; 4 + import { api } from '../utils/api'; 4 5 import useTitle from '../utils/useTitle'; 5 6 6 7 const LIMIT = 20; 7 8 8 9 function Bookmarks() { 9 10 useTitle('Bookmarks', '/b'); 11 + const { masto } = api(); 10 12 const bookmarksIterator = useRef(); 11 13 async function fetchBookmarks(firstLoad) { 12 14 if (firstLoad || !bookmarksIterator.current) {
+2
src/pages/favourites.jsx
··· 1 1 import { useRef } from 'preact/hooks'; 2 2 3 3 import Timeline from '../components/timeline'; 4 + import { api } from '../utils/api'; 4 5 import useTitle from '../utils/useTitle'; 5 6 6 7 const LIMIT = 20; 7 8 8 9 function Favourites() { 9 10 useTitle('Favourites', '/f'); 11 + const { masto } = api(); 10 12 const favouritesIterator = useRef(); 11 13 async function fetchFavourites(firstLoad) { 12 14 if (firstLoad || !favouritesIterator.current) {
+2
src/pages/following.jsx
··· 2 2 import { useSnapshot } from 'valtio'; 3 3 4 4 import Timeline from '../components/timeline'; 5 + import { api } from '../utils/api'; 5 6 import useTitle from '../utils/useTitle'; 6 7 7 8 const LIMIT = 20; 8 9 9 10 function Following() { 10 11 useTitle('Following', '/l/f'); 12 + const { masto } = api(); 11 13 const snapStates = useSnapshot(states); 12 14 const homeIterator = useRef(); 13 15 async function fetchHome(firstLoad) {
+12 -2
src/pages/hashtags.jsx
··· 2 2 import { useParams } from 'react-router-dom'; 3 3 4 4 import Timeline from '../components/timeline'; 5 + import { api } from '../utils/api'; 5 6 import useTitle from '../utils/useTitle'; 6 7 7 8 const LIMIT = 20; 8 9 9 10 function Hashtags() { 10 - const { hashtag } = useParams(); 11 + const { hashtag, instance } = useParams(); 11 12 useTitle(`#${hashtag}`, `/t/${hashtag}`); 13 + const { masto } = api({ instance }); 12 14 const hashtagsIterator = useRef(); 13 15 async function fetchHashtags(firstLoad) { 14 16 if (firstLoad || !hashtagsIterator.current) { ··· 22 24 return ( 23 25 <Timeline 24 26 key={hashtag} 25 - title={`#${hashtag}`} 27 + title={instance ? `#${hashtag} on ${instance}` : `#${hashtag}`} 28 + titleComponent={ 29 + !!instance && ( 30 + <h1 class="header-account"> 31 + <b>#{hashtag}</b> 32 + <div>{instance}</div> 33 + </h1> 34 + ) 35 + } 26 36 id="hashtags" 27 37 emptyText="No one has posted anything with this tag yet." 28 38 errorText="Unable to load posts with this tag"
+2
src/pages/home.jsx
··· 8 8 import Link from '../components/link'; 9 9 import Loader from '../components/loader'; 10 10 import Status from '../components/status'; 11 + import { api } from '../utils/api'; 11 12 import db from '../utils/db'; 12 13 import states, { saveStatus } from '../utils/states'; 13 14 import { getCurrentAccountNS } from '../utils/store-utils'; ··· 18 19 19 20 function Home({ hidden }) { 20 21 useTitle('Home', '/'); 22 + const { masto } = api(); 21 23 const snapStates = useSnapshot(states); 22 24 const isHomeLocation = snapStates.currentLocation === '/'; 23 25 const [uiState, setUIState] = useState('default');
+2
src/pages/lists.jsx
··· 2 2 import { useParams } from 'react-router-dom'; 3 3 4 4 import Timeline from '../components/timeline'; 5 + import { api } from '../utils/api'; 5 6 import useTitle from '../utils/useTitle'; 6 7 7 8 const LIMIT = 20; 8 9 9 10 function Lists() { 11 + const { masto } = api(); 10 12 const { id } = useParams(); 11 13 const listsIterator = useRef(); 12 14 async function fetchLists(firstLoad) {
+2
src/pages/notifications.jsx
··· 11 11 import NameText from '../components/name-text'; 12 12 import RelativeTime from '../components/relative-time'; 13 13 import Status from '../components/status'; 14 + import { api } from '../utils/api'; 14 15 import states, { saveStatus } from '../utils/states'; 15 16 import store from '../utils/store'; 16 17 import useScroll from '../utils/useScroll'; ··· 48 49 49 50 function Notifications() { 50 51 useTitle('Notifications', '/notifications'); 52 + const { masto } = api(); 51 53 const snapStates = useSnapshot(states); 52 54 const [uiState, setUIState] = useState('default'); 53 55 const [showMore, setShowMore] = useState(false);
+19 -50
src/pages/public.jsx
··· 1 1 // EXPERIMENTAL: This is a work in progress and may not work as expected. 2 + import { useRef } from 'preact/hooks'; 2 3 import { useMatch, useParams } from 'react-router-dom'; 3 4 4 5 import Timeline from '../components/timeline'; 6 + import { api } from '../utils/api'; 5 7 import useTitle from '../utils/useTitle'; 6 8 7 9 const LIMIT = 20; 8 10 9 - let nextUrl = null; 10 - 11 11 function Public() { 12 12 const isLocal = !!useMatch('/p/l/:instance'); 13 - const params = useParams(); 14 - const { instance = '' } = params; 13 + const { instance } = useParams(); 14 + const { masto } = api({ instance }); 15 15 const title = `${instance} (${isLocal ? 'local' : 'federated'})`; 16 16 useTitle(title, `/p/${instance}`); 17 + 18 + const publicIterator = useRef(); 17 19 async function fetchPublic(firstLoad) { 18 - const url = firstLoad 19 - ? `https://${instance}/api/v1/timelines/public?limit=${LIMIT}&local=${isLocal}` 20 - : nextUrl; 21 - if (!url) return { values: [], done: true }; 22 - const response = await fetch(url); 23 - let value = await response.json(); 24 - if (value) { 25 - value = camelCaseKeys(value); 20 + if (firstLoad || !publicIterator.current) { 21 + publicIterator.current = masto.v1.timelines.listPublic({ 22 + limit: LIMIT, 23 + local: isLocal, 24 + }); 26 25 } 27 - const done = !response.headers.has('link'); 28 - nextUrl = done 29 - ? null 30 - : response.headers.get('link').match(/<(.+?)>; rel="next"/)?.[1]; 31 - console.debug({ 32 - url, 33 - value, 34 - done, 35 - nextUrl, 36 - }); 37 - return { value, done }; 26 + return await publicIterator.current.next(); 38 27 } 39 28 40 29 return ( 41 30 <Timeline 42 31 key={instance + isLocal} 43 32 title={title} 33 + titleComponent={ 34 + <h1 class="header-account"> 35 + <b>{instance}</b> 36 + <div>{isLocal ? 'local' : 'federated'}</div> 37 + </h1> 38 + } 44 39 id="public" 40 + instance={instance} 45 41 emptyText="No one has posted anything yet." 46 42 errorText="Unable to load posts" 47 43 fetchItems={fetchPublic} 48 44 /> 49 45 ); 50 - } 51 - 52 - function camelCaseKeys(obj) { 53 - if (Array.isArray(obj)) { 54 - return obj.map((item) => camelCaseKeys(item)); 55 - } 56 - return new Proxy(obj, { 57 - get(target, prop) { 58 - let value = undefined; 59 - if (prop in target) { 60 - value = target[prop]; 61 - } 62 - if (!value) { 63 - const snakeCaseProp = prop.replace( 64 - /([A-Z])/g, 65 - (g) => `_${g.toLowerCase()}`, 66 - ); 67 - if (snakeCaseProp in target) { 68 - value = target[snakeCaseProp]; 69 - } 70 - } 71 - if (value && typeof value === 'object') { 72 - return camelCaseKeys(value); 73 - } 74 - return value; 75 - }, 76 - }); 77 46 } 78 47 79 48 export default Public;
+6 -1
src/pages/settings.jsx
··· 10 10 import Link from '../components/link'; 11 11 import NameText from '../components/name-text'; 12 12 import RelativeTime from '../components/relative-time'; 13 + import { api } from '../utils/api'; 13 14 import states from '../utils/states'; 14 15 import store from '../utils/store'; 15 16 ··· 20 21 */ 21 22 22 23 function Settings({ onClose }) { 24 + const { masto } = api(); 23 25 const snapStates = useSnapshot(states); 24 26 // Accounts 25 27 const accounts = store.local.getJSON('accounts'); ··· 178 180 } 179 181 document 180 182 .querySelector('meta[name="color-scheme"]') 181 - .setAttribute('content', theme); 183 + .setAttribute( 184 + 'content', 185 + theme === 'auto' ? 'dark light' : theme, 186 + ); 182 187 183 188 if (theme === 'auto') { 184 189 store.local.del('theme');
+34 -10
src/pages/status.jsx
··· 6 6 import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; 7 7 import { useHotkeys } from 'react-hotkeys-hook'; 8 8 import { InView } from 'react-intersection-observer'; 9 - import { useLocation, useNavigate, useParams } from 'react-router-dom'; 9 + import { useNavigate, useParams } from 'react-router-dom'; 10 10 import { useDebouncedCallback } from 'use-debounce'; 11 11 import { useSnapshot } from 'valtio'; 12 12 ··· 17 17 import NameText from '../components/name-text'; 18 18 import RelativeTime from '../components/relative-time'; 19 19 import Status from '../components/status'; 20 + import { api } from '../utils/api'; 20 21 import htmlContentLength from '../utils/html-content-length'; 21 22 import shortenNumber from '../utils/shorten-number'; 22 23 import states, { saveStatus, threadifyStatus } from '../utils/states'; ··· 34 35 } 35 36 36 37 function StatusPage() { 37 - const { id } = useParams(); 38 - const location = useLocation(); 38 + const { id, instance } = useParams(); 39 + const { masto } = api({ instance }); 39 40 const navigate = useNavigate(); 40 41 const snapStates = useSnapshot(states); 41 42 const [statuses, setStatuses] = useState([]); ··· 92 93 } 93 94 94 95 (async () => { 96 + console.log('MASTO V1 fetch', masto); 95 97 const heroFetch = () => 96 98 pRetry(() => masto.v1.statuses.fetch(id), { 97 99 retries: 4, ··· 211 213 }; 212 214 }; 213 215 214 - useEffect(initContext, [id]); 216 + useEffect(initContext, [id, masto]); 215 217 useEffect(() => { 216 218 if (!statuses.length) return; 217 219 console.debug('STATUSES', statuses); ··· 462 464 {!heroInView && heroStatus && uiState !== 'loading' ? ( 463 465 <> 464 466 <span class="hero-heading"> 465 - <NameText showAvatar account={heroStatus.account} short />{' '} 467 + <NameText 468 + account={heroStatus.account} 469 + instance={instance} 470 + showAvatar 471 + short 472 + />{' '} 466 473 <span class="insignificant"> 467 474 &bull;{' '} 468 475 <RelativeTime ··· 583 590 class="status-focus" 584 591 tabIndex={0} 585 592 > 586 - <Status statusID={statusID} withinContext size="l" /> 593 + <Status 594 + statusID={statusID} 595 + instance={instance} 596 + withinContext 597 + size="l" 598 + /> 587 599 </InView> 588 600 ) : ( 589 601 <Link 590 602 class="status-link" 591 - to={`/s/${statusID}`} 603 + to={ 604 + instance 605 + ? `/s/${instance}/${statusID}` 606 + : `/s/${statusID}` 607 + } 592 608 onClick={() => { 593 609 resetScrollPosition(statusID); 594 610 }} 595 611 > 596 612 <Status 597 613 statusID={statusID} 614 + instance={instance} 598 615 withinContext 599 616 size={thread || ancestor ? 'm' : 's'} 600 617 /> ··· 610 627 )} 611 628 {descendant && replies?.length > 0 && ( 612 629 <SubComments 630 + instance={instance} 613 631 hasManyStatuses={hasManyStatuses} 614 632 replies={replies} 615 633 /> ··· 691 709 ); 692 710 } 693 711 694 - function SubComments({ hasManyStatuses, replies }) { 712 + function SubComments({ hasManyStatuses, replies, instance }) { 695 713 // Set isBrief = true: 696 714 // - if less than or 2 replies 697 715 // - if replies have no sub-replies ··· 764 782 <li key={r.id}> 765 783 <Link 766 784 class="status-link" 767 - to={`/s/${r.id}`} 785 + to={instance ? `/s/${instance}/${r.id}` : `/s/${r.id}`} 768 786 onClick={() => { 769 787 resetScrollPosition(r.id); 770 788 }} 771 789 > 772 - <Status statusID={r.id} withinContext size="s" /> 790 + <Status 791 + statusID={r.id} 792 + instance={instance} 793 + withinContext 794 + size="s" 795 + /> 773 796 {!r.replies?.length && r.repliesCount > 0 && ( 774 797 <div class="replies-link"> 775 798 <Icon icon="comment" />{' '} ··· 781 804 </Link> 782 805 {r.replies?.length && ( 783 806 <SubComments 807 + instance={instance} 784 808 hasManyStatuses={hasManyStatuses} 785 809 replies={r.replies} 786 810 />
+176
src/utils/api.js
··· 1 + import { createClient } from 'masto'; 2 + 3 + import store from './store'; 4 + import { getAccount, getCurrentAccount, saveAccount } from './store-utils'; 5 + 6 + // Default *fallback* instance 7 + const DEFAULT_INSTANCE = 'mastodon.social'; 8 + 9 + // Per-instance masto instance 10 + // Useful when only one account is logged in 11 + // I'm not sure if I'll ever allow multiple logged-in accounts but oh well... 12 + // E.g. apis['mastodon.social'] 13 + const apis = {}; 14 + 15 + // Per-account masto instance 16 + // Note: There can be many accounts per instance 17 + // Useful when multiple accounts are logged in or when certain actions require a specific account 18 + // Just in case if I need this one day. 19 + // E.g. accountApis['mastodon.social']['ACCESS_TOKEN'] 20 + const accountApis = {}; 21 + 22 + // Current account masto instance 23 + let currentAccountApi; 24 + 25 + export function initClient({ instance, accessToken }) { 26 + if (/^https?:\/\//.test(instance)) { 27 + instance = instance 28 + .replace(/^https?:\/\//, '') 29 + .replace(/\/+$/, '') 30 + .toLowerCase(); 31 + } 32 + const url = instance ? `https://${instance}` : `https://${DEFAULT_INSTANCE}`; 33 + 34 + const client = createClient({ 35 + url, 36 + accessToken, // Can be null 37 + disableVersionCheck: true, // Allow non-Mastodon instances 38 + timeout: 30_000, // Unfortunatly this is global instead of per-request 39 + }); 40 + client.__instance__ = instance; 41 + 42 + apis[instance] = client; 43 + if (!accountApis[instance]) accountApis[instance] = {}; 44 + if (accessToken) accountApis[instance][accessToken] = client; 45 + 46 + return client; 47 + } 48 + 49 + // Get the instance information 50 + // The config is needed for composing 51 + export async function initInstance(client) { 52 + const masto = client; 53 + // Request v2, fallback to v1 if fail 54 + let info; 55 + try { 56 + info = await masto.v2.instance.fetch(); 57 + } catch (e) {} 58 + if (!info) { 59 + try { 60 + info = await masto.v1.instances.fetch(); 61 + } catch (e) {} 62 + } 63 + if (!info) return; 64 + console.log(info); 65 + const { 66 + // v1 67 + uri, 68 + urls: { streamingApi } = {}, 69 + // v2 70 + domain, 71 + configuration: { urls: { streaming } = {} } = {}, 72 + } = info; 73 + if (uri || domain) { 74 + const instances = store.local.getJSON('instances') || {}; 75 + instances[ 76 + (domain || uri) 77 + .replace(/^https?:\/\//, '') 78 + .replace(/\/+$/, '') 79 + .toLowerCase() 80 + ] = info; 81 + store.local.setJSON('instances', instances); 82 + } 83 + // This is a weird place to put this but here's updating the masto instance with the streaming API URL set in the configuration 84 + // Reason: Streaming WebSocket URL may change, unlike the standard API REST URLs 85 + if (streamingApi || streaming) { 86 + masto.config.props.streamingApiUrl = streaming || streamingApi; 87 + } 88 + } 89 + 90 + // Get the account information and store it 91 + export async function initAccount(client, instance, accessToken) { 92 + const masto = client; 93 + const mastoAccount = await masto.v1.accounts.verifyCredentials(); 94 + 95 + saveAccount({ 96 + info: mastoAccount, 97 + instanceURL: instance.toLowerCase(), 98 + accessToken, 99 + }); 100 + } 101 + 102 + // Get the masto instance 103 + // If accountID is provided, get the masto instance for that account 104 + export function api({ instance, accessToken, accountID, account } = {}) { 105 + // If instance and accessToken are provided, get the masto instance for that account 106 + if (instance && accessToken) { 107 + return { 108 + masto: 109 + accountApis[instance]?.[accessToken] || 110 + initClient({ instance, accessToken }), 111 + authenticated: true, 112 + instance, 113 + }; 114 + } 115 + 116 + // If account is provided, get the masto instance for that account 117 + if (account || accountID) { 118 + account = account || getAccount(accountID); 119 + if (account) { 120 + const accessToken = account.accessToken; 121 + const instance = account.instanceURL; 122 + return { 123 + masto: 124 + accountApis[instance]?.[accessToken] || 125 + initClient({ instance, accessToken }), 126 + authenticated: true, 127 + instance, 128 + }; 129 + } else { 130 + throw new Error(`Account ${accountID} not found`); 131 + } 132 + } 133 + 134 + // If only instance is provided, get the masto instance for that instance 135 + if (instance) { 136 + const masto = apis[instance] || initClient({ instance }); 137 + return { 138 + masto, 139 + authenticated: !!masto.config.props.accessToken, 140 + instance, 141 + }; 142 + } 143 + 144 + // If no instance is provided, get the masto instance for the current account 145 + if (currentAccountApi) 146 + return { 147 + masto: currentAccountApi, 148 + authenticated: true, 149 + instance: currentAccountApi.__instance__, 150 + }; 151 + const currentAccount = getCurrentAccount(); 152 + if (currentAccount) { 153 + const { accessToken, instanceURL: instance } = currentAccount; 154 + currentAccountApi = 155 + accountApis[instance]?.[accessToken] || 156 + initClient({ instance, accessToken }); 157 + return { 158 + masto: currentAccountApi, 159 + authenticated: true, 160 + instance, 161 + }; 162 + } 163 + 164 + // If no instance is provided and no account is logged in, get the masto instance for DEFAULT_INSTANCE 165 + return { 166 + masto: apis[DEFAULT_INSTANCE] || initClient({ instance: DEFAULT_INSTANCE }), 167 + authenticated: false, 168 + instance: DEFAULT_INSTANCE, 169 + }; 170 + } 171 + 172 + window.__API__ = { 173 + currentAccountApi, 174 + apis, 175 + accountApis, 176 + };
+12 -4
src/utils/handle-content-links.js
··· 1 1 import states from './states'; 2 2 3 3 function handleContentLinks(opts) { 4 - const { mentions = [] } = opts || {}; 4 + const { mentions = [], instance } = opts || {}; 5 5 return (e) => { 6 6 let { target } = e; 7 7 if (target.parentNode.tagName.toLowerCase() === 'a') { ··· 25 25 if (mention) { 26 26 e.preventDefault(); 27 27 e.stopPropagation(); 28 - states.showAccount = mention.acct; 28 + states.showAccount = { 29 + account: mention.acct, 30 + instance, 31 + }; 29 32 } else if (!/^http/i.test(targetText)) { 30 33 console.log('mention not found', targetText); 31 34 e.preventDefault(); 32 35 e.stopPropagation(); 33 36 const href = target.getAttribute('href'); 34 - states.showAccount = href; 37 + states.showAccount = { 38 + account: href, 39 + instance, 40 + }; 35 41 } 36 42 } else if ( 37 43 target.tagName.toLowerCase() === 'a' && ··· 40 46 e.preventDefault(); 41 47 e.stopPropagation(); 42 48 const tag = target.innerText.replace(/^#/, '').trim(); 43 - location.hash = `#/t/${tag}`; 49 + const hashURL = instance ? `#/t/${instance}/${tag}` : `#/t/${tag}`; 50 + console.log({ hashURL }); 51 + location.hash = hashURL; 44 52 } 45 53 }; 46 54 }
+3 -3
src/utils/open-compose.js
··· 13 13 ); 14 14 15 15 if (newWin) { 16 - if (masto) { 17 - newWin.masto = masto; 18 - } 16 + // if (masto) { 17 + // newWin.masto = masto; 18 + // } 19 19 20 20 newWin.__COMPOSE__ = opts; 21 21 }
+2
src/utils/states.js
··· 1 1 import { proxy } from 'valtio'; 2 2 import { subscribeKey } from 'valtio/utils'; 3 3 4 + import { api } from './api'; 4 5 import store from './store'; 5 6 6 7 const states = proxy({ ··· 76 77 } 77 78 78 79 export function threadifyStatus(status) { 80 + const { masto } = api(); 79 81 // Return all statuses in the thread, via inReplyToId, if inReplyToAccountId === account.id 80 82 let fetchIndex = 0; 81 83 async function traverse(status, index = 0) {
+20 -3
src/utils/store-utils.js
··· 1 1 import store from './store'; 2 2 3 - export function getCurrentAccount() { 3 + export function getAccount(id) { 4 4 const accounts = store.local.getJSON('accounts') || []; 5 + return accounts.find((a) => a.info.id === id); 6 + } 7 + 8 + export function getCurrentAccount() { 5 9 const currentAccount = store.session.get('currentAccount'); 6 - const account = 7 - accounts.find((a) => a.info.id === currentAccount) || accounts[0]; 10 + const account = getAccount(currentAccount); 8 11 return account; 9 12 } 10 13 ··· 16 19 } = account; 17 20 return `${id}@${instanceURL}`; 18 21 } 22 + 23 + export function saveAccount(account) { 24 + const accounts = store.local.getJSON('accounts') || []; 25 + const acc = accounts.find((a) => a.info.id === account.info.id); 26 + if (acc) { 27 + acc.info = account.info; 28 + acc.instanceURL = account.instanceURL; 29 + acc.accessToken = account.accessToken; 30 + } else { 31 + accounts.push(account); 32 + } 33 + store.local.setJSON('accounts', accounts); 34 + store.session.set('currentAccount', account.info.id); 35 + }