mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-16 15:21:48 +03:00
Breaking: refactor all masto API calls
Everything need to be instance-aware!
This commit is contained in:
parent
b47c043699
commit
a130743d4c
25 changed files with 481 additions and 253 deletions
130
src/app.jsx
130
src/app.jsx
|
@ -2,7 +2,6 @@ import './app.css';
|
|||
import 'toastify-js/src/toastify.css';
|
||||
|
||||
import debounce from 'just-debounce-it';
|
||||
import { createClient } from 'masto';
|
||||
import {
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
|
@ -36,9 +35,11 @@ import Public from './pages/public';
|
|||
import Settings from './pages/settings';
|
||||
import Status from './pages/status';
|
||||
import Welcome from './pages/welcome';
|
||||
import { api, initAccount, initClient, initInstance } from './utils/api';
|
||||
import { getAccessToken } from './utils/auth';
|
||||
import states, { saveStatus } from './utils/states';
|
||||
import store from './utils/store';
|
||||
import { getCurrentAccount } from './utils/store-utils';
|
||||
|
||||
window.__STATES__ = states;
|
||||
|
||||
|
@ -54,13 +55,12 @@ function App() {
|
|||
document.documentElement.classList.add(`is-${theme}`);
|
||||
document
|
||||
.querySelector('meta[name="color-scheme"]')
|
||||
.setAttribute('content', theme);
|
||||
.setAttribute('content', theme === 'auto' ? 'dark light' : theme);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const instanceURL = store.local.get('instanceURL');
|
||||
const accounts = store.local.getJSON('accounts') || [];
|
||||
const code = (window.location.search.match(/code=([^&]+)/) || [])[1];
|
||||
|
||||
if (code) {
|
||||
|
@ -73,58 +73,31 @@ function App() {
|
|||
|
||||
(async () => {
|
||||
setUIState('loading');
|
||||
const tokenJSON = await getAccessToken({
|
||||
const { access_token: accessToken } = await getAccessToken({
|
||||
instanceURL,
|
||||
client_id: clientID,
|
||||
client_secret: clientSecret,
|
||||
code,
|
||||
});
|
||||
const { access_token: accessToken } = tokenJSON;
|
||||
store.session.set('accessToken', accessToken);
|
||||
|
||||
initMasto({
|
||||
url: `https://${instanceURL}`,
|
||||
accessToken,
|
||||
});
|
||||
|
||||
const mastoAccount = await masto.v1.accounts.verifyCredentials();
|
||||
|
||||
// console.log({ tokenJSON, mastoAccount });
|
||||
|
||||
let account = accounts.find((a) => a.info.id === mastoAccount.id);
|
||||
if (account) {
|
||||
account.info = mastoAccount;
|
||||
account.instanceURL = instanceURL.toLowerCase();
|
||||
account.accessToken = accessToken;
|
||||
} else {
|
||||
account = {
|
||||
info: mastoAccount,
|
||||
instanceURL,
|
||||
accessToken,
|
||||
};
|
||||
accounts.push(account);
|
||||
}
|
||||
|
||||
store.local.setJSON('accounts', accounts);
|
||||
store.session.set('currentAccount', account.info.id);
|
||||
const masto = initClient({ instance: instanceURL, accessToken });
|
||||
await Promise.allSettled([
|
||||
initInstance(masto),
|
||||
initAccount(masto, instanceURL, accessToken),
|
||||
]);
|
||||
|
||||
setIsLoggedIn(true);
|
||||
setUIState('default');
|
||||
})();
|
||||
} else if (accounts.length) {
|
||||
const currentAccount = store.session.get('currentAccount');
|
||||
const account =
|
||||
accounts.find((a) => a.info.id === currentAccount) || accounts[0];
|
||||
const instanceURL = account.instanceURL;
|
||||
const accessToken = account.accessToken;
|
||||
store.session.set('currentAccount', account.info.id);
|
||||
if (accessToken) setIsLoggedIn(true);
|
||||
|
||||
initMasto({
|
||||
url: `https://${instanceURL}`,
|
||||
accessToken,
|
||||
});
|
||||
} else {
|
||||
const account = getCurrentAccount();
|
||||
if (account) {
|
||||
store.session.set('currentAccount', account.info.id);
|
||||
const { masto } = api({ account });
|
||||
initInstance(masto);
|
||||
setIsLoggedIn(true);
|
||||
}
|
||||
|
||||
setUIState('default');
|
||||
}
|
||||
}, []);
|
||||
|
@ -181,9 +154,11 @@ function App() {
|
|||
|
||||
const nonRootLocation = useMemo(() => {
|
||||
const { pathname } = location;
|
||||
return !/^\/(login|welcome|p)/.test(pathname);
|
||||
return !/^\/(login|welcome)/.test(pathname);
|
||||
}, [location]);
|
||||
|
||||
console.log('nonRootLocation', nonRootLocation, 'location', location);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Routes location={nonRootLocation || location}>
|
||||
|
@ -210,13 +185,17 @@ function App() {
|
|||
{isLoggedIn && <Route path="/b" element={<Bookmarks />} />}
|
||||
{isLoggedIn && <Route path="/f" element={<Favourites />} />}
|
||||
{isLoggedIn && <Route path="/l/:id" element={<Lists />} />}
|
||||
{isLoggedIn && <Route path="/t/:hashtag" element={<Hashtags />} />}
|
||||
{isLoggedIn && <Route path="/a/:id" element={<AccountStatuses />} />}
|
||||
{isLoggedIn && (
|
||||
<Route path="/t/:instance?/:hashtag" element={<Hashtags />} />
|
||||
)}
|
||||
{isLoggedIn && (
|
||||
<Route path="/a/:instance?/:id" element={<AccountStatuses />} />
|
||||
)}
|
||||
<Route path="/p/l?/:instance" element={<Public />} />
|
||||
{/* <Route path="/:anything" element={<NotFound />} /> */}
|
||||
</Routes>
|
||||
<Routes>
|
||||
<Route path="/s/:id" element={<Status />} />
|
||||
<Route path="/s/:instance?/:id" element={<Status />} />
|
||||
</Routes>
|
||||
<nav id="tab-bar" hidden>
|
||||
<li>
|
||||
|
@ -304,7 +283,8 @@ function App() {
|
|||
}}
|
||||
>
|
||||
<Account
|
||||
account={snapStates.showAccount}
|
||||
account={snapStates.showAccount?.account || snapStates.showAccount}
|
||||
instance={snapStates.showAccount?.instance}
|
||||
onClose={() => {
|
||||
states.showAccount = false;
|
||||
}}
|
||||
|
@ -335,6 +315,7 @@ function App() {
|
|||
>
|
||||
<MediaModal
|
||||
mediaAttachments={snapStates.showMediaModal.mediaAttachments}
|
||||
instance={snapStates.showMediaModal.instance}
|
||||
index={snapStates.showMediaModal.index}
|
||||
statusID={snapStates.showMediaModal.statusID}
|
||||
onClose={() => {
|
||||
|
@ -347,57 +328,9 @@ function App() {
|
|||
);
|
||||
}
|
||||
|
||||
function initMasto(params) {
|
||||
const clientParams = {
|
||||
url: params.url || 'https://mastodon.social',
|
||||
accessToken: params.accessToken || null,
|
||||
disableVersionCheck: true,
|
||||
timeout: 30_000,
|
||||
};
|
||||
window.masto = createClient(clientParams);
|
||||
|
||||
(async () => {
|
||||
// Request v2, fallback to v1 if fail
|
||||
let info;
|
||||
try {
|
||||
info = await masto.v2.instance.fetch();
|
||||
} catch (e) {}
|
||||
if (!info) {
|
||||
try {
|
||||
info = await masto.v1.instances.fetch();
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!info) return;
|
||||
console.log(info);
|
||||
const {
|
||||
// v1
|
||||
uri,
|
||||
urls: { streamingApi } = {},
|
||||
// v2
|
||||
domain,
|
||||
configuration: { urls: { streaming } = {} } = {},
|
||||
} = info;
|
||||
if (uri || domain) {
|
||||
const instances = store.local.getJSON('instances') || {};
|
||||
instances[
|
||||
(domain || uri)
|
||||
.replace(/^https?:\/\//, '')
|
||||
.replace(/\/+$/, '')
|
||||
.toLowerCase()
|
||||
] = info;
|
||||
store.local.setJSON('instances', instances);
|
||||
}
|
||||
if (streamingApi || streaming) {
|
||||
window.masto = createClient({
|
||||
...clientParams,
|
||||
streamingApiUrl: streaming || streamingApi,
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
let ws;
|
||||
async function startStream() {
|
||||
const { masto } = api();
|
||||
if (
|
||||
ws &&
|
||||
(ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)
|
||||
|
@ -472,6 +405,7 @@ async function startStream() {
|
|||
|
||||
let lastHidden;
|
||||
function startVisibility() {
|
||||
const { masto } = api();
|
||||
const handleVisible = (visible) => {
|
||||
if (!visible) {
|
||||
const timestamp = Date.now();
|
||||
|
|
|
@ -2,6 +2,7 @@ import './account.css';
|
|||
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
|
||||
import { api } from '../utils/api';
|
||||
import emojifyText from '../utils/emojify-text';
|
||||
import enhanceContent from '../utils/enhance-content';
|
||||
import handleContentLinks from '../utils/handle-content-links';
|
||||
|
@ -14,7 +15,8 @@ import Icon from './icon';
|
|||
import Link from './link';
|
||||
import NameText from './name-text';
|
||||
|
||||
function Account({ account, onClose }) {
|
||||
function Account({ account, instance, onClose }) {
|
||||
const { masto, authenticated } = api({ instance });
|
||||
const [uiState, setUIState] = useState('default');
|
||||
const isString = typeof account === 'string';
|
||||
const [info, setInfo] = useState(isString ? null : account);
|
||||
|
@ -82,7 +84,7 @@ function Account({ account, onClose }) {
|
|||
const [relationship, setRelationship] = useState(null);
|
||||
const [familiarFollowers, setFamiliarFollowers] = useState([]);
|
||||
useEffect(() => {
|
||||
if (info) {
|
||||
if (info && authenticated) {
|
||||
const currentAccount = store.session.get('currentAccount');
|
||||
if (currentAccount === id) {
|
||||
// It's myself!
|
||||
|
@ -120,7 +122,7 @@ function Account({ account, onClose }) {
|
|||
}
|
||||
})();
|
||||
}
|
||||
}, [info]);
|
||||
}, [info, authenticated]);
|
||||
|
||||
const {
|
||||
following,
|
||||
|
@ -174,7 +176,7 @@ function Account({ account, onClose }) {
|
|||
<>
|
||||
<header>
|
||||
<Avatar url={avatar} size="xxxl" />
|
||||
<NameText account={info} showAcct external />
|
||||
<NameText account={info} instance={instance} showAcct external />
|
||||
</header>
|
||||
<main tabIndex="-1">
|
||||
{bot && (
|
||||
|
@ -186,7 +188,9 @@ function Account({ account, onClose }) {
|
|||
)}
|
||||
<div
|
||||
class="note"
|
||||
onClick={handleContentLinks()}
|
||||
onClick={handleContentLinks({
|
||||
instance,
|
||||
})}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: enhanceContent(note, { emojis }),
|
||||
}}
|
||||
|
@ -270,7 +274,10 @@ function Account({ account, onClose }) {
|
|||
rel="noopener noreferrer"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
states.showAccount = follower;
|
||||
states.showAccount = {
|
||||
account: follower,
|
||||
instance,
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
|
|
|
@ -12,6 +12,7 @@ import { useSnapshot } from 'valtio';
|
|||
|
||||
import supportedLanguages from '../data/status-supported-languages';
|
||||
import urlRegex from '../data/url-regex';
|
||||
import { api } from '../utils/api';
|
||||
import db from '../utils/db';
|
||||
import emojifyText from '../utils/emojify-text';
|
||||
import openCompose from '../utils/open-compose';
|
||||
|
@ -99,6 +100,7 @@ function Compose({
|
|||
hasOpener,
|
||||
}) {
|
||||
console.warn('RENDER COMPOSER');
|
||||
const { masto } = api();
|
||||
const [uiState, setUIState] = useState('default');
|
||||
const UID = useRef(draftStatus?.uid || uid());
|
||||
console.log('Compose UID', UID.current);
|
||||
|
@ -868,6 +870,9 @@ function Compose({
|
|||
updateCharCount();
|
||||
}}
|
||||
maxCharacters={maxCharacters}
|
||||
performSearch={(params) => {
|
||||
return masto.v2.search(params);
|
||||
}}
|
||||
/>
|
||||
{mediaAttachments.length > 0 && (
|
||||
<div class="media-attachments">
|
||||
|
@ -1031,7 +1036,7 @@ function Compose({
|
|||
|
||||
const Textarea = forwardRef((props, ref) => {
|
||||
const [text, setText] = useState(ref.current?.value || '');
|
||||
const { maxCharacters, ...textareaProps } = props;
|
||||
const { maxCharacters, performSearch = () => {}, ...textareaProps } = props;
|
||||
const snapStates = useSnapshot(states);
|
||||
const charCount = snapStates.composerCharacterCount;
|
||||
|
||||
|
@ -1087,7 +1092,7 @@ const Textarea = forwardRef((props, ref) => {
|
|||
}[key];
|
||||
provide(
|
||||
new Promise((resolve) => {
|
||||
const searchResults = masto.v2.search({
|
||||
const searchResults = performSearch({
|
||||
type,
|
||||
q: text,
|
||||
limit: 5,
|
||||
|
|
|
@ -2,6 +2,7 @@ import './drafts.css';
|
|||
|
||||
import { useEffect, useMemo, useReducer, useState } from 'react';
|
||||
|
||||
import { api } from '../utils/api';
|
||||
import db from '../utils/db';
|
||||
import states from '../utils/states';
|
||||
import { getCurrentAccountNS } from '../utils/store-utils';
|
||||
|
@ -10,6 +11,7 @@ import Icon from './icon';
|
|||
import Loader from './loader';
|
||||
|
||||
function Drafts() {
|
||||
const { masto } = api();
|
||||
const [uiState, setUIState] = useState('default');
|
||||
const [drafts, setDrafts] = useState([]);
|
||||
const [reloadCount, reload] = useReducer((c) => c + 1, 0);
|
||||
|
|
|
@ -11,11 +11,12 @@ import Modal from './modal';
|
|||
function MediaModal({
|
||||
mediaAttachments,
|
||||
statusID,
|
||||
instance,
|
||||
index = 0,
|
||||
onClose = () => {},
|
||||
}) {
|
||||
const carouselRef = useRef(null);
|
||||
const isStatusLocation = useMatch('/s/:id');
|
||||
const isStatusLocation = useMatch('/s/:instance?/:id');
|
||||
|
||||
const [currentIndex, setCurrentIndex] = useState(index);
|
||||
const carouselFocusItem = useRef(null);
|
||||
|
@ -167,7 +168,7 @@ function MediaModal({
|
|||
<span>
|
||||
{!isStatusLocation && (
|
||||
<Link
|
||||
to={`/s/${statusID}`}
|
||||
to={instance ? `/s/${instance}/${statusID}` : `/s/${statusID}`}
|
||||
class="button carousel-button media-post-link plain3"
|
||||
onClick={() => {
|
||||
// if small screen (not media query min-width 40em + 350px), run onClose
|
||||
|
|
|
@ -5,7 +5,15 @@ import states from '../utils/states';
|
|||
|
||||
import Avatar from './avatar';
|
||||
|
||||
function NameText({ account, showAvatar, showAcct, short, external, onClick }) {
|
||||
function NameText({
|
||||
account,
|
||||
instance,
|
||||
showAvatar,
|
||||
showAcct,
|
||||
short,
|
||||
external,
|
||||
onClick,
|
||||
}) {
|
||||
const { acct, avatar, avatarStatic, id, url, displayName, emojis } = account;
|
||||
let { username } = account;
|
||||
|
||||
|
@ -34,7 +42,10 @@ function NameText({ account, showAvatar, showAcct, short, external, onClick }) {
|
|||
if (external) return;
|
||||
e.preventDefault();
|
||||
if (onClick) return onClick(e);
|
||||
states.showAccount = account;
|
||||
states.showAccount = {
|
||||
account,
|
||||
instance,
|
||||
};
|
||||
}}
|
||||
>
|
||||
{showAvatar && (
|
||||
|
|
|
@ -1,17 +1,9 @@
|
|||
import './status.css';
|
||||
|
||||
import { Menu, MenuItem } from '@szhsin/react-menu';
|
||||
import { getBlurHashAverageColor } from 'fast-blurhash';
|
||||
import mem from 'mem';
|
||||
import { memo } from 'preact/compat';
|
||||
import {
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'preact/hooks';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||
import 'swiped-events';
|
||||
import useResizeObserver from 'use-resize-observer';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
@ -19,6 +11,7 @@ import { useSnapshot } from 'valtio';
|
|||
import Loader from '../components/loader';
|
||||
import Modal from '../components/modal';
|
||||
import NameText from '../components/name-text';
|
||||
import { api } from '../utils/api';
|
||||
import enhanceContent from '../utils/enhance-content';
|
||||
import handleContentLinks from '../utils/handle-content-links';
|
||||
import htmlContentLength from '../utils/html-content-length';
|
||||
|
@ -33,7 +26,7 @@ import Link from './link';
|
|||
import Media from './media';
|
||||
import RelativeTime from './relative-time';
|
||||
|
||||
function fetchAccount(id) {
|
||||
function fetchAccount(id, masto) {
|
||||
try {
|
||||
return masto.v1.accounts.fetch(id);
|
||||
} catch (e) {
|
||||
|
@ -45,6 +38,7 @@ const memFetchAccount = mem(fetchAccount);
|
|||
function Status({
|
||||
statusID,
|
||||
status,
|
||||
instance,
|
||||
withinContext,
|
||||
size = 'm',
|
||||
skeleton,
|
||||
|
@ -65,6 +59,7 @@ function Status({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
const { masto, authenticated } = api({ instance });
|
||||
|
||||
const snapStates = useSnapshot(states);
|
||||
if (!status) {
|
||||
|
@ -135,7 +130,7 @@ function Status({
|
|||
if (account) {
|
||||
setInReplyToAccount(account);
|
||||
} else {
|
||||
memFetchAccount(inReplyToAccountId)
|
||||
memFetchAccount(inReplyToAccountId, masto)
|
||||
.then((account) => {
|
||||
setInReplyToAccount(account);
|
||||
states.accounts[account.id] = account;
|
||||
|
@ -157,9 +152,10 @@ function Status({
|
|||
<div class="status-reblog" onMouseEnter={debugHover}>
|
||||
<div class="status-pre-meta">
|
||||
<Icon icon="rocket" size="l" />{' '}
|
||||
<NameText account={status.account} showAvatar /> boosted
|
||||
<NameText account={status.account} instance={instance} showAvatar />{' '}
|
||||
boosted
|
||||
</div>
|
||||
<Status status={reblog} size={size} />
|
||||
<Status status={reblog} instance={instance} size={size} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -198,6 +194,8 @@ function Status({
|
|||
|
||||
const statusRef = useRef(null);
|
||||
|
||||
const unauthInteractionErrorMessage = `Sorry, your current logged-in instance can't interact with this status from another instance.`;
|
||||
|
||||
return (
|
||||
<article
|
||||
ref={statusRef}
|
||||
|
@ -229,7 +227,10 @@ function Status({
|
|||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
states.showAccount = status.account;
|
||||
states.showAccount = {
|
||||
account: status.account,
|
||||
instance,
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Avatar url={avatarStatic} size="xxl" />
|
||||
|
@ -240,6 +241,7 @@ function Status({
|
|||
{/* <span> */}
|
||||
<NameText
|
||||
account={status.account}
|
||||
instance={instance}
|
||||
showAvatar={size === 's'}
|
||||
showAcct={size === 'l'}
|
||||
/>
|
||||
|
@ -248,14 +250,23 @@ function Status({
|
|||
{' '}
|
||||
<span class="ib">
|
||||
<Icon icon="arrow-right" class="arrow" />{' '}
|
||||
<NameText account={inReplyToAccount} short />
|
||||
<NameText account={inReplyToAccount} instance={instance} short />
|
||||
</span>
|
||||
</>
|
||||
)} */}
|
||||
{/* </span> */}{' '}
|
||||
{size !== 'l' &&
|
||||
(uri ? (
|
||||
<Link to={`/s/${id}`} class="time">
|
||||
<Link
|
||||
to={
|
||||
instance
|
||||
? `
|
||||
/s/${instance}/${id}
|
||||
`
|
||||
: `/s/${id}`
|
||||
}
|
||||
class="time"
|
||||
>
|
||||
<Icon
|
||||
icon={visibilityIconsMap[visibility]}
|
||||
alt={visibility}
|
||||
|
@ -294,7 +305,11 @@ function Status({
|
|||
})) && (
|
||||
<div class="status-reply-badge">
|
||||
<Icon icon="reply" />{' '}
|
||||
<NameText account={inReplyToAccount} short />
|
||||
<NameText
|
||||
account={inReplyToAccount}
|
||||
instance={instance}
|
||||
short
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
@ -346,7 +361,7 @@ function Status({
|
|||
lang={language}
|
||||
ref={contentRef}
|
||||
data-read-more={readMoreText}
|
||||
onClick={handleContentLinks({ mentions })}
|
||||
onClick={handleContentLinks({ mentions, instance })}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: enhanceContent(content, {
|
||||
emojis,
|
||||
|
@ -367,10 +382,28 @@ function Status({
|
|||
<Poll
|
||||
lang={language}
|
||||
poll={poll}
|
||||
readOnly={readOnly}
|
||||
readOnly={readOnly || !authenticated}
|
||||
onUpdate={(newPoll) => {
|
||||
states.statuses[id].poll = newPoll;
|
||||
}}
|
||||
refresh={() => {
|
||||
return masto.v1.polls
|
||||
.fetch(poll.id)
|
||||
.then((pollResponse) => {
|
||||
states.statuses[id].poll = pollResponse;
|
||||
})
|
||||
.catch((e) => {}); // Silently fail
|
||||
}}
|
||||
votePoll={(choices) => {
|
||||
return masto.v1.polls
|
||||
.vote(poll.id, {
|
||||
choices,
|
||||
})
|
||||
.then((pollResponse) => {
|
||||
states.statuses[id].poll = pollResponse;
|
||||
})
|
||||
.catch((e) => {}); // Silently fail
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!spoilerText && sensitive && !!mediaAttachments.length && (
|
||||
|
@ -410,6 +443,7 @@ function Status({
|
|||
states.showMediaModal = {
|
||||
mediaAttachments,
|
||||
index: i,
|
||||
instance,
|
||||
statusID: readOnly ? null : id,
|
||||
};
|
||||
}}
|
||||
|
@ -477,6 +511,9 @@ function Status({
|
|||
icon="comment"
|
||||
count={repliesCount}
|
||||
onClick={() => {
|
||||
if (!authenticated) {
|
||||
return alert(unauthInteractionErrorMessage);
|
||||
}
|
||||
states.showCompose = {
|
||||
replyToStatus: status,
|
||||
};
|
||||
|
@ -494,6 +531,9 @@ function Status({
|
|||
icon="rocket"
|
||||
count={reblogsCount}
|
||||
onClick={async () => {
|
||||
if (!authenticated) {
|
||||
return alert(unauthInteractionErrorMessage);
|
||||
}
|
||||
try {
|
||||
if (!reblogged) {
|
||||
const yes = confirm(
|
||||
|
@ -536,6 +576,9 @@ function Status({
|
|||
icon="heart"
|
||||
count={favouritesCount}
|
||||
onClick={async () => {
|
||||
if (!authenticated) {
|
||||
return alert(unauthInteractionErrorMessage);
|
||||
}
|
||||
try {
|
||||
// Optimistic
|
||||
states.statuses[statusID] = {
|
||||
|
@ -569,6 +612,9 @@ function Status({
|
|||
class="bookmark-button"
|
||||
icon="bookmark"
|
||||
onClick={async () => {
|
||||
if (!authenticated) {
|
||||
return alert(unauthInteractionErrorMessage);
|
||||
}
|
||||
try {
|
||||
// Optimistic
|
||||
states.statuses[statusID] = {
|
||||
|
@ -635,6 +681,10 @@ function Status({
|
|||
>
|
||||
<EditedAtModal
|
||||
statusID={showEdited}
|
||||
instance={instance}
|
||||
fetchStatusHistory={() => {
|
||||
return masto.v1.statuses.listHistory(showEdited);
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowEdited(false);
|
||||
statusRef.current?.focus();
|
||||
|
@ -742,7 +792,13 @@ function Card({ card }) {
|
|||
}
|
||||
}
|
||||
|
||||
function Poll({ poll, lang, readOnly, onUpdate = () => {} }) {
|
||||
function Poll({
|
||||
poll,
|
||||
lang,
|
||||
readOnly,
|
||||
refresh = () => {},
|
||||
votePoll = () => {},
|
||||
}) {
|
||||
const [uiState, setUIState] = useState('default');
|
||||
|
||||
const {
|
||||
|
@ -768,12 +824,7 @@ function Poll({ poll, lang, readOnly, onUpdate = () => {} }) {
|
|||
timeout = setTimeout(() => {
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
const pollResponse = await masto.v1.polls.fetch(id);
|
||||
onUpdate(pollResponse);
|
||||
} catch (e) {
|
||||
// Silent fail
|
||||
}
|
||||
await refresh();
|
||||
setUIState('default');
|
||||
})();
|
||||
}, ms);
|
||||
|
@ -847,19 +898,15 @@ function Poll({ poll, lang, readOnly, onUpdate = () => {} }) {
|
|||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const formData = new FormData(form);
|
||||
const votes = [];
|
||||
const choices = [];
|
||||
formData.forEach((value, key) => {
|
||||
if (key === 'poll') {
|
||||
votes.push(value);
|
||||
choices.push(value);
|
||||
}
|
||||
});
|
||||
console.log(votes);
|
||||
setUIState('loading');
|
||||
const pollResponse = await masto.v1.polls.vote(id, {
|
||||
choices: votes,
|
||||
});
|
||||
console.log(pollResponse);
|
||||
onUpdate(pollResponse);
|
||||
await votePoll(choices);
|
||||
setUIState('default');
|
||||
}}
|
||||
>
|
||||
|
@ -903,12 +950,7 @@ function Poll({ poll, lang, readOnly, onUpdate = () => {} }) {
|
|||
e.preventDefault();
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
const pollResponse = await masto.v1.polls.fetch(id);
|
||||
onUpdate(pollResponse);
|
||||
} catch (e) {
|
||||
// Silent fail
|
||||
}
|
||||
await refresh();
|
||||
setUIState('default');
|
||||
})();
|
||||
}}
|
||||
|
@ -937,7 +979,12 @@ function Poll({ poll, lang, readOnly, onUpdate = () => {} }) {
|
|||
);
|
||||
}
|
||||
|
||||
function EditedAtModal({ statusID, onClose = () => {} }) {
|
||||
function EditedAtModal({
|
||||
statusID,
|
||||
instance,
|
||||
fetchStatusHistory = () => {},
|
||||
onClose = () => {},
|
||||
}) {
|
||||
const [uiState, setUIState] = useState('default');
|
||||
const [editHistory, setEditHistory] = useState([]);
|
||||
|
||||
|
@ -945,7 +992,7 @@ function EditedAtModal({ statusID, onClose = () => {} }) {
|
|||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
const editHistory = await masto.v1.statuses.listHistory(statusID);
|
||||
const editHistory = await fetchStatusHistory();
|
||||
console.log(editHistory);
|
||||
setEditHistory(editHistory);
|
||||
setUIState('default');
|
||||
|
@ -997,7 +1044,13 @@ function EditedAtModal({ statusID, onClose = () => {} }) {
|
|||
}).format(createdAtDate)}
|
||||
</time>
|
||||
</h3>
|
||||
<Status status={status} size="s" withinContext readOnly />
|
||||
<Status
|
||||
status={status}
|
||||
instance={instance}
|
||||
size="s"
|
||||
withinContext
|
||||
readOnly
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -12,6 +12,7 @@ function Timeline({
|
|||
title,
|
||||
titleComponent,
|
||||
id,
|
||||
instance,
|
||||
emptyText,
|
||||
errorText,
|
||||
boostsCarousel,
|
||||
|
@ -112,17 +113,20 @@ function Timeline({
|
|||
{items.map((status) => {
|
||||
const { id: statusID, reblog, boosts } = status;
|
||||
const actualStatusID = reblog?.id || statusID;
|
||||
const url = instance
|
||||
? `/s/${instance}/${actualStatusID}`
|
||||
: `/s/${actualStatusID}`;
|
||||
if (boosts) {
|
||||
return (
|
||||
<li key={`timeline-${statusID}`}>
|
||||
<BoostsCarousel boosts={boosts} />
|
||||
<BoostsCarousel boosts={boosts} instance={instance} />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<li key={`timeline-${statusID}`}>
|
||||
<Link class="status-link" to={`/s/${actualStatusID}`}>
|
||||
<Status status={status} />
|
||||
<Link class="status-link" to={url}>
|
||||
<Status status={status} instance={instance} />
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
|
@ -213,7 +217,7 @@ function groupBoosts(values) {
|
|||
}
|
||||
}
|
||||
|
||||
function BoostsCarousel({ boosts }) {
|
||||
function BoostsCarousel({ boosts, instance }) {
|
||||
const carouselRef = useRef();
|
||||
const { reachStart, reachEnd, init } = useScroll({
|
||||
scrollableElement: carouselRef.current,
|
||||
|
@ -260,10 +264,13 @@ function BoostsCarousel({ boosts }) {
|
|||
{boosts.map((boost) => {
|
||||
const { id: statusID, reblog } = boost;
|
||||
const actualStatusID = reblog?.id || statusID;
|
||||
const url = instance
|
||||
? `/s/${instance}/${actualStatusID}`
|
||||
: `/s/${actualStatusID}`;
|
||||
return (
|
||||
<li key={statusID}>
|
||||
<Link class="status-boost-link" to={`/s/${actualStatusID}`}>
|
||||
<Status status={boost} size="s" />
|
||||
<Link class="status-boost-link" to={url}>
|
||||
<Status status={boost} instance={instance} size="s" />
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
|
|
|
@ -2,36 +2,16 @@ import './index.css';
|
|||
|
||||
import './app.css';
|
||||
|
||||
import { createClient } from 'masto';
|
||||
import { render } from 'preact';
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
|
||||
import Compose from './components/compose';
|
||||
import { getCurrentAccount } from './utils/store-utils';
|
||||
import useTitle from './utils/useTitle';
|
||||
|
||||
if (window.opener) {
|
||||
console = window.opener.console;
|
||||
}
|
||||
|
||||
(() => {
|
||||
if (window.masto) return;
|
||||
console.warn('window.masto not found. Trying to log in...');
|
||||
try {
|
||||
const { instanceURL, accessToken } = getCurrentAccount();
|
||||
window.masto = createClient({
|
||||
url: `https://${instanceURL}`,
|
||||
accessToken,
|
||||
disableVersionCheck: true,
|
||||
timeout: 30_000,
|
||||
});
|
||||
console.info('Logged in successfully.');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Failed to log in. Please try again.');
|
||||
}
|
||||
})();
|
||||
|
||||
function App() {
|
||||
const [uiState, setUIState] = useState('default');
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { useParams } from 'react-router-dom';
|
|||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import Timeline from '../components/timeline';
|
||||
import { api } from '../utils/api';
|
||||
import emojifyText from '../utils/emojify-text';
|
||||
import states from '../utils/states';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
@ -11,7 +12,8 @@ const LIMIT = 20;
|
|||
|
||||
function AccountStatuses() {
|
||||
const snapStates = useSnapshot(states);
|
||||
const { id } = useParams();
|
||||
const { id, instance } = useParams();
|
||||
const { masto } = api({ instance });
|
||||
const accountStatusesIterator = useRef();
|
||||
async function fetchAccountStatuses(firstLoad) {
|
||||
if (firstLoad || !accountStatusesIterator.current) {
|
||||
|
@ -46,7 +48,10 @@ function AccountStatuses() {
|
|||
<h1
|
||||
class="header-account"
|
||||
onClick={() => {
|
||||
states.showAccount = account;
|
||||
states.showAccount = {
|
||||
account,
|
||||
instance,
|
||||
};
|
||||
}}
|
||||
>
|
||||
<b
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { useRef } from 'preact/hooks';
|
||||
|
||||
import Timeline from '../components/timeline';
|
||||
import { api } from '../utils/api';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
||||
function Bookmarks() {
|
||||
useTitle('Bookmarks', '/b');
|
||||
const { masto } = api();
|
||||
const bookmarksIterator = useRef();
|
||||
async function fetchBookmarks(firstLoad) {
|
||||
if (firstLoad || !bookmarksIterator.current) {
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { useRef } from 'preact/hooks';
|
||||
|
||||
import Timeline from '../components/timeline';
|
||||
import { api } from '../utils/api';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
||||
function Favourites() {
|
||||
useTitle('Favourites', '/f');
|
||||
const { masto } = api();
|
||||
const favouritesIterator = useRef();
|
||||
async function fetchFavourites(firstLoad) {
|
||||
if (firstLoad || !favouritesIterator.current) {
|
||||
|
|
|
@ -2,12 +2,14 @@ import { useRef } from 'preact/hooks';
|
|||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import Timeline from '../components/timeline';
|
||||
import { api } from '../utils/api';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
||||
function Following() {
|
||||
useTitle('Following', '/l/f');
|
||||
const { masto } = api();
|
||||
const snapStates = useSnapshot(states);
|
||||
const homeIterator = useRef();
|
||||
async function fetchHome(firstLoad) {
|
||||
|
|
|
@ -2,13 +2,15 @@ import { useRef } from 'preact/hooks';
|
|||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import Timeline from '../components/timeline';
|
||||
import { api } from '../utils/api';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
||||
function Hashtags() {
|
||||
const { hashtag } = useParams();
|
||||
const { hashtag, instance } = useParams();
|
||||
useTitle(`#${hashtag}`, `/t/${hashtag}`);
|
||||
const { masto } = api({ instance });
|
||||
const hashtagsIterator = useRef();
|
||||
async function fetchHashtags(firstLoad) {
|
||||
if (firstLoad || !hashtagsIterator.current) {
|
||||
|
@ -22,7 +24,15 @@ function Hashtags() {
|
|||
return (
|
||||
<Timeline
|
||||
key={hashtag}
|
||||
title={`#${hashtag}`}
|
||||
title={instance ? `#${hashtag} on ${instance}` : `#${hashtag}`}
|
||||
titleComponent={
|
||||
!!instance && (
|
||||
<h1 class="header-account">
|
||||
<b>#{hashtag}</b>
|
||||
<div>{instance}</div>
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
id="hashtags"
|
||||
emptyText="No one has posted anything with this tag yet."
|
||||
errorText="Unable to load posts with this tag"
|
||||
|
|
|
@ -8,6 +8,7 @@ import Icon from '../components/icon';
|
|||
import Link from '../components/link';
|
||||
import Loader from '../components/loader';
|
||||
import Status from '../components/status';
|
||||
import { api } from '../utils/api';
|
||||
import db from '../utils/db';
|
||||
import states, { saveStatus } from '../utils/states';
|
||||
import { getCurrentAccountNS } from '../utils/store-utils';
|
||||
|
@ -18,6 +19,7 @@ const LIMIT = 20;
|
|||
|
||||
function Home({ hidden }) {
|
||||
useTitle('Home', '/');
|
||||
const { masto } = api();
|
||||
const snapStates = useSnapshot(states);
|
||||
const isHomeLocation = snapStates.currentLocation === '/';
|
||||
const [uiState, setUIState] = useState('default');
|
||||
|
|
|
@ -2,11 +2,13 @@ import { useEffect, useRef, useState } from 'preact/hooks';
|
|||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import Timeline from '../components/timeline';
|
||||
import { api } from '../utils/api';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
||||
function Lists() {
|
||||
const { masto } = api();
|
||||
const { id } = useParams();
|
||||
const listsIterator = useRef();
|
||||
async function fetchLists(firstLoad) {
|
||||
|
|
|
@ -11,6 +11,7 @@ import Loader from '../components/loader';
|
|||
import NameText from '../components/name-text';
|
||||
import RelativeTime from '../components/relative-time';
|
||||
import Status from '../components/status';
|
||||
import { api } from '../utils/api';
|
||||
import states, { saveStatus } from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
import useScroll from '../utils/useScroll';
|
||||
|
@ -48,6 +49,7 @@ const LIMIT = 30; // 30 is the maximum limit :(
|
|||
|
||||
function Notifications() {
|
||||
useTitle('Notifications', '/notifications');
|
||||
const { masto } = api();
|
||||
const snapStates = useSnapshot(states);
|
||||
const [uiState, setUIState] = useState('default');
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
|
|
|
@ -1,47 +1,43 @@
|
|||
// EXPERIMENTAL: This is a work in progress and may not work as expected.
|
||||
import { useRef } from 'preact/hooks';
|
||||
import { useMatch, useParams } from 'react-router-dom';
|
||||
|
||||
import Timeline from '../components/timeline';
|
||||
import { api } from '../utils/api';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
const LIMIT = 20;
|
||||
|
||||
let nextUrl = null;
|
||||
|
||||
function Public() {
|
||||
const isLocal = !!useMatch('/p/l/:instance');
|
||||
const params = useParams();
|
||||
const { instance = '' } = params;
|
||||
const { instance } = useParams();
|
||||
const { masto } = api({ instance });
|
||||
const title = `${instance} (${isLocal ? 'local' : 'federated'})`;
|
||||
useTitle(title, `/p/${instance}`);
|
||||
|
||||
const publicIterator = useRef();
|
||||
async function fetchPublic(firstLoad) {
|
||||
const url = firstLoad
|
||||
? `https://${instance}/api/v1/timelines/public?limit=${LIMIT}&local=${isLocal}`
|
||||
: nextUrl;
|
||||
if (!url) return { values: [], done: true };
|
||||
const response = await fetch(url);
|
||||
let value = await response.json();
|
||||
if (value) {
|
||||
value = camelCaseKeys(value);
|
||||
if (firstLoad || !publicIterator.current) {
|
||||
publicIterator.current = masto.v1.timelines.listPublic({
|
||||
limit: LIMIT,
|
||||
local: isLocal,
|
||||
});
|
||||
}
|
||||
const done = !response.headers.has('link');
|
||||
nextUrl = done
|
||||
? null
|
||||
: response.headers.get('link').match(/<(.+?)>; rel="next"/)?.[1];
|
||||
console.debug({
|
||||
url,
|
||||
value,
|
||||
done,
|
||||
nextUrl,
|
||||
});
|
||||
return { value, done };
|
||||
return await publicIterator.current.next();
|
||||
}
|
||||
|
||||
return (
|
||||
<Timeline
|
||||
key={instance + isLocal}
|
||||
title={title}
|
||||
titleComponent={
|
||||
<h1 class="header-account">
|
||||
<b>{instance}</b>
|
||||
<div>{isLocal ? 'local' : 'federated'}</div>
|
||||
</h1>
|
||||
}
|
||||
id="public"
|
||||
instance={instance}
|
||||
emptyText="No one has posted anything yet."
|
||||
errorText="Unable to load posts"
|
||||
fetchItems={fetchPublic}
|
||||
|
@ -49,31 +45,4 @@ function Public() {
|
|||
);
|
||||
}
|
||||
|
||||
function camelCaseKeys(obj) {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => camelCaseKeys(item));
|
||||
}
|
||||
return new Proxy(obj, {
|
||||
get(target, prop) {
|
||||
let value = undefined;
|
||||
if (prop in target) {
|
||||
value = target[prop];
|
||||
}
|
||||
if (!value) {
|
||||
const snakeCaseProp = prop.replace(
|
||||
/([A-Z])/g,
|
||||
(g) => `_${g.toLowerCase()}`,
|
||||
);
|
||||
if (snakeCaseProp in target) {
|
||||
value = target[snakeCaseProp];
|
||||
}
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
return camelCaseKeys(value);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default Public;
|
||||
|
|
|
@ -10,6 +10,7 @@ import Icon from '../components/icon';
|
|||
import Link from '../components/link';
|
||||
import NameText from '../components/name-text';
|
||||
import RelativeTime from '../components/relative-time';
|
||||
import { api } from '../utils/api';
|
||||
import states from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
|
||||
|
@ -20,6 +21,7 @@ import store from '../utils/store';
|
|||
*/
|
||||
|
||||
function Settings({ onClose }) {
|
||||
const { masto } = api();
|
||||
const snapStates = useSnapshot(states);
|
||||
// Accounts
|
||||
const accounts = store.local.getJSON('accounts');
|
||||
|
@ -178,7 +180,10 @@ function Settings({ onClose }) {
|
|||
}
|
||||
document
|
||||
.querySelector('meta[name="color-scheme"]')
|
||||
.setAttribute('content', theme);
|
||||
.setAttribute(
|
||||
'content',
|
||||
theme === 'auto' ? 'dark light' : theme,
|
||||
);
|
||||
|
||||
if (theme === 'auto') {
|
||||
store.local.del('theme');
|
||||
|
|
|
@ -6,7 +6,7 @@ import pRetry from 'p-retry';
|
|||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { InView } from 'react-intersection-observer';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
|
@ -17,6 +17,7 @@ import Loader from '../components/loader';
|
|||
import NameText from '../components/name-text';
|
||||
import RelativeTime from '../components/relative-time';
|
||||
import Status from '../components/status';
|
||||
import { api } from '../utils/api';
|
||||
import htmlContentLength from '../utils/html-content-length';
|
||||
import shortenNumber from '../utils/shorten-number';
|
||||
import states, { saveStatus, threadifyStatus } from '../utils/states';
|
||||
|
@ -34,8 +35,8 @@ function resetScrollPosition(id) {
|
|||
}
|
||||
|
||||
function StatusPage() {
|
||||
const { id } = useParams();
|
||||
const location = useLocation();
|
||||
const { id, instance } = useParams();
|
||||
const { masto } = api({ instance });
|
||||
const navigate = useNavigate();
|
||||
const snapStates = useSnapshot(states);
|
||||
const [statuses, setStatuses] = useState([]);
|
||||
|
@ -92,6 +93,7 @@ function StatusPage() {
|
|||
}
|
||||
|
||||
(async () => {
|
||||
console.log('MASTO V1 fetch', masto);
|
||||
const heroFetch = () =>
|
||||
pRetry(() => masto.v1.statuses.fetch(id), {
|
||||
retries: 4,
|
||||
|
@ -211,7 +213,7 @@ function StatusPage() {
|
|||
};
|
||||
};
|
||||
|
||||
useEffect(initContext, [id]);
|
||||
useEffect(initContext, [id, masto]);
|
||||
useEffect(() => {
|
||||
if (!statuses.length) return;
|
||||
console.debug('STATUSES', statuses);
|
||||
|
@ -462,7 +464,12 @@ function StatusPage() {
|
|||
{!heroInView && heroStatus && uiState !== 'loading' ? (
|
||||
<>
|
||||
<span class="hero-heading">
|
||||
<NameText showAvatar account={heroStatus.account} short />{' '}
|
||||
<NameText
|
||||
account={heroStatus.account}
|
||||
instance={instance}
|
||||
showAvatar
|
||||
short
|
||||
/>{' '}
|
||||
<span class="insignificant">
|
||||
•{' '}
|
||||
<RelativeTime
|
||||
|
@ -583,18 +590,28 @@ function StatusPage() {
|
|||
class="status-focus"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Status statusID={statusID} withinContext size="l" />
|
||||
<Status
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
withinContext
|
||||
size="l"
|
||||
/>
|
||||
</InView>
|
||||
) : (
|
||||
<Link
|
||||
class="status-link"
|
||||
to={`/s/${statusID}`}
|
||||
to={
|
||||
instance
|
||||
? `/s/${instance}/${statusID}`
|
||||
: `/s/${statusID}`
|
||||
}
|
||||
onClick={() => {
|
||||
resetScrollPosition(statusID);
|
||||
}}
|
||||
>
|
||||
<Status
|
||||
statusID={statusID}
|
||||
instance={instance}
|
||||
withinContext
|
||||
size={thread || ancestor ? 'm' : 's'}
|
||||
/>
|
||||
|
@ -610,6 +627,7 @@ function StatusPage() {
|
|||
)}
|
||||
{descendant && replies?.length > 0 && (
|
||||
<SubComments
|
||||
instance={instance}
|
||||
hasManyStatuses={hasManyStatuses}
|
||||
replies={replies}
|
||||
/>
|
||||
|
@ -691,7 +709,7 @@ function StatusPage() {
|
|||
);
|
||||
}
|
||||
|
||||
function SubComments({ hasManyStatuses, replies }) {
|
||||
function SubComments({ hasManyStatuses, replies, instance }) {
|
||||
// Set isBrief = true:
|
||||
// - if less than or 2 replies
|
||||
// - if replies have no sub-replies
|
||||
|
@ -764,12 +782,17 @@ function SubComments({ hasManyStatuses, replies }) {
|
|||
<li key={r.id}>
|
||||
<Link
|
||||
class="status-link"
|
||||
to={`/s/${r.id}`}
|
||||
to={instance ? `/s/${instance}/${r.id}` : `/s/${r.id}`}
|
||||
onClick={() => {
|
||||
resetScrollPosition(r.id);
|
||||
}}
|
||||
>
|
||||
<Status statusID={r.id} withinContext size="s" />
|
||||
<Status
|
||||
statusID={r.id}
|
||||
instance={instance}
|
||||
withinContext
|
||||
size="s"
|
||||
/>
|
||||
{!r.replies?.length && r.repliesCount > 0 && (
|
||||
<div class="replies-link">
|
||||
<Icon icon="comment" />{' '}
|
||||
|
@ -781,6 +804,7 @@ function SubComments({ hasManyStatuses, replies }) {
|
|||
</Link>
|
||||
{r.replies?.length && (
|
||||
<SubComments
|
||||
instance={instance}
|
||||
hasManyStatuses={hasManyStatuses}
|
||||
replies={r.replies}
|
||||
/>
|
||||
|
|
176
src/utils/api.js
Normal file
176
src/utils/api.js
Normal file
|
@ -0,0 +1,176 @@
|
|||
import { createClient } from 'masto';
|
||||
|
||||
import store from './store';
|
||||
import { getAccount, getCurrentAccount, saveAccount } from './store-utils';
|
||||
|
||||
// Default *fallback* instance
|
||||
const DEFAULT_INSTANCE = 'mastodon.social';
|
||||
|
||||
// Per-instance masto instance
|
||||
// Useful when only one account is logged in
|
||||
// I'm not sure if I'll ever allow multiple logged-in accounts but oh well...
|
||||
// E.g. apis['mastodon.social']
|
||||
const apis = {};
|
||||
|
||||
// Per-account masto instance
|
||||
// Note: There can be many accounts per instance
|
||||
// Useful when multiple accounts are logged in or when certain actions require a specific account
|
||||
// Just in case if I need this one day.
|
||||
// E.g. accountApis['mastodon.social']['ACCESS_TOKEN']
|
||||
const accountApis = {};
|
||||
|
||||
// Current account masto instance
|
||||
let currentAccountApi;
|
||||
|
||||
export function initClient({ instance, accessToken }) {
|
||||
if (/^https?:\/\//.test(instance)) {
|
||||
instance = instance
|
||||
.replace(/^https?:\/\//, '')
|
||||
.replace(/\/+$/, '')
|
||||
.toLowerCase();
|
||||
}
|
||||
const url = instance ? `https://${instance}` : `https://${DEFAULT_INSTANCE}`;
|
||||
|
||||
const client = createClient({
|
||||
url,
|
||||
accessToken, // Can be null
|
||||
disableVersionCheck: true, // Allow non-Mastodon instances
|
||||
timeout: 30_000, // Unfortunatly this is global instead of per-request
|
||||
});
|
||||
client.__instance__ = instance;
|
||||
|
||||
apis[instance] = client;
|
||||
if (!accountApis[instance]) accountApis[instance] = {};
|
||||
if (accessToken) accountApis[instance][accessToken] = client;
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
// Get the instance information
|
||||
// The config is needed for composing
|
||||
export async function initInstance(client) {
|
||||
const masto = client;
|
||||
// Request v2, fallback to v1 if fail
|
||||
let info;
|
||||
try {
|
||||
info = await masto.v2.instance.fetch();
|
||||
} catch (e) {}
|
||||
if (!info) {
|
||||
try {
|
||||
info = await masto.v1.instances.fetch();
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!info) return;
|
||||
console.log(info);
|
||||
const {
|
||||
// v1
|
||||
uri,
|
||||
urls: { streamingApi } = {},
|
||||
// v2
|
||||
domain,
|
||||
configuration: { urls: { streaming } = {} } = {},
|
||||
} = info;
|
||||
if (uri || domain) {
|
||||
const instances = store.local.getJSON('instances') || {};
|
||||
instances[
|
||||
(domain || uri)
|
||||
.replace(/^https?:\/\//, '')
|
||||
.replace(/\/+$/, '')
|
||||
.toLowerCase()
|
||||
] = info;
|
||||
store.local.setJSON('instances', instances);
|
||||
}
|
||||
// This is a weird place to put this but here's updating the masto instance with the streaming API URL set in the configuration
|
||||
// Reason: Streaming WebSocket URL may change, unlike the standard API REST URLs
|
||||
if (streamingApi || streaming) {
|
||||
masto.config.props.streamingApiUrl = streaming || streamingApi;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the account information and store it
|
||||
export async function initAccount(client, instance, accessToken) {
|
||||
const masto = client;
|
||||
const mastoAccount = await masto.v1.accounts.verifyCredentials();
|
||||
|
||||
saveAccount({
|
||||
info: mastoAccount,
|
||||
instanceURL: instance.toLowerCase(),
|
||||
accessToken,
|
||||
});
|
||||
}
|
||||
|
||||
// Get the masto instance
|
||||
// If accountID is provided, get the masto instance for that account
|
||||
export function api({ instance, accessToken, accountID, account } = {}) {
|
||||
// If instance and accessToken are provided, get the masto instance for that account
|
||||
if (instance && accessToken) {
|
||||
return {
|
||||
masto:
|
||||
accountApis[instance]?.[accessToken] ||
|
||||
initClient({ instance, accessToken }),
|
||||
authenticated: true,
|
||||
instance,
|
||||
};
|
||||
}
|
||||
|
||||
// If account is provided, get the masto instance for that account
|
||||
if (account || accountID) {
|
||||
account = account || getAccount(accountID);
|
||||
if (account) {
|
||||
const accessToken = account.accessToken;
|
||||
const instance = account.instanceURL;
|
||||
return {
|
||||
masto:
|
||||
accountApis[instance]?.[accessToken] ||
|
||||
initClient({ instance, accessToken }),
|
||||
authenticated: true,
|
||||
instance,
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Account ${accountID} not found`);
|
||||
}
|
||||
}
|
||||
|
||||
// If only instance is provided, get the masto instance for that instance
|
||||
if (instance) {
|
||||
const masto = apis[instance] || initClient({ instance });
|
||||
return {
|
||||
masto,
|
||||
authenticated: !!masto.config.props.accessToken,
|
||||
instance,
|
||||
};
|
||||
}
|
||||
|
||||
// If no instance is provided, get the masto instance for the current account
|
||||
if (currentAccountApi)
|
||||
return {
|
||||
masto: currentAccountApi,
|
||||
authenticated: true,
|
||||
instance: currentAccountApi.__instance__,
|
||||
};
|
||||
const currentAccount = getCurrentAccount();
|
||||
if (currentAccount) {
|
||||
const { accessToken, instanceURL: instance } = currentAccount;
|
||||
currentAccountApi =
|
||||
accountApis[instance]?.[accessToken] ||
|
||||
initClient({ instance, accessToken });
|
||||
return {
|
||||
masto: currentAccountApi,
|
||||
authenticated: true,
|
||||
instance,
|
||||
};
|
||||
}
|
||||
|
||||
// If no instance is provided and no account is logged in, get the masto instance for DEFAULT_INSTANCE
|
||||
return {
|
||||
masto: apis[DEFAULT_INSTANCE] || initClient({ instance: DEFAULT_INSTANCE }),
|
||||
authenticated: false,
|
||||
instance: DEFAULT_INSTANCE,
|
||||
};
|
||||
}
|
||||
|
||||
window.__API__ = {
|
||||
currentAccountApi,
|
||||
apis,
|
||||
accountApis,
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
import states from './states';
|
||||
|
||||
function handleContentLinks(opts) {
|
||||
const { mentions = [] } = opts || {};
|
||||
const { mentions = [], instance } = opts || {};
|
||||
return (e) => {
|
||||
let { target } = e;
|
||||
if (target.parentNode.tagName.toLowerCase() === 'a') {
|
||||
|
@ -25,13 +25,19 @@ function handleContentLinks(opts) {
|
|||
if (mention) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
states.showAccount = mention.acct;
|
||||
states.showAccount = {
|
||||
account: mention.acct,
|
||||
instance,
|
||||
};
|
||||
} else if (!/^http/i.test(targetText)) {
|
||||
console.log('mention not found', targetText);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const href = target.getAttribute('href');
|
||||
states.showAccount = href;
|
||||
states.showAccount = {
|
||||
account: href,
|
||||
instance,
|
||||
};
|
||||
}
|
||||
} else if (
|
||||
target.tagName.toLowerCase() === 'a' &&
|
||||
|
@ -40,7 +46,9 @@ function handleContentLinks(opts) {
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const tag = target.innerText.replace(/^#/, '').trim();
|
||||
location.hash = `#/t/${tag}`;
|
||||
const hashURL = instance ? `#/t/${instance}/${tag}` : `#/t/${tag}`;
|
||||
console.log({ hashURL });
|
||||
location.hash = hashURL;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -13,9 +13,9 @@ export default function openCompose(opts) {
|
|||
);
|
||||
|
||||
if (newWin) {
|
||||
if (masto) {
|
||||
newWin.masto = masto;
|
||||
}
|
||||
// if (masto) {
|
||||
// newWin.masto = masto;
|
||||
// }
|
||||
|
||||
newWin.__COMPOSE__ = opts;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { proxy } from 'valtio';
|
||||
import { subscribeKey } from 'valtio/utils';
|
||||
|
||||
import { api } from './api';
|
||||
import store from './store';
|
||||
|
||||
const states = proxy({
|
||||
|
@ -76,6 +77,7 @@ export function saveStatus(status, opts) {
|
|||
}
|
||||
|
||||
export function threadifyStatus(status) {
|
||||
const { masto } = api();
|
||||
// Return all statuses in the thread, via inReplyToId, if inReplyToAccountId === account.id
|
||||
let fetchIndex = 0;
|
||||
async function traverse(status, index = 0) {
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import store from './store';
|
||||
|
||||
export function getCurrentAccount() {
|
||||
export function getAccount(id) {
|
||||
const accounts = store.local.getJSON('accounts') || [];
|
||||
return accounts.find((a) => a.info.id === id);
|
||||
}
|
||||
|
||||
export function getCurrentAccount() {
|
||||
const currentAccount = store.session.get('currentAccount');
|
||||
const account =
|
||||
accounts.find((a) => a.info.id === currentAccount) || accounts[0];
|
||||
const account = getAccount(currentAccount);
|
||||
return account;
|
||||
}
|
||||
|
||||
|
@ -16,3 +19,17 @@ export function getCurrentAccountNS() {
|
|||
} = account;
|
||||
return `${id}@${instanceURL}`;
|
||||
}
|
||||
|
||||
export function saveAccount(account) {
|
||||
const accounts = store.local.getJSON('accounts') || [];
|
||||
const acc = accounts.find((a) => a.info.id === account.info.id);
|
||||
if (acc) {
|
||||
acc.info = account.info;
|
||||
acc.instanceURL = account.instanceURL;
|
||||
acc.accessToken = account.accessToken;
|
||||
} else {
|
||||
accounts.push(account);
|
||||
}
|
||||
store.local.setJSON('accounts', accounts);
|
||||
store.session.set('currentAccount', account.info.id);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue