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