2022-12-10 12:14:48 +03:00
|
|
|
import './app.css';
|
2022-12-26 09:02:05 +03:00
|
|
|
import 'toastify-js/src/toastify.css';
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
|
|
import { createHashHistory } from 'history';
|
2022-12-13 11:39:21 +03:00
|
|
|
import { login } from 'masto';
|
2022-12-26 09:02:05 +03:00
|
|
|
import Router, { route } from 'preact-router';
|
2022-12-10 12:14:48 +03:00
|
|
|
import { useEffect, useLayoutEffect, useState } from 'preact/hooks';
|
2022-12-26 09:02:05 +03:00
|
|
|
import Toastify from 'toastify-js';
|
2022-12-10 12:14:48 +03:00
|
|
|
import { useSnapshot } from 'valtio';
|
|
|
|
|
|
|
|
import Account from './components/account';
|
|
|
|
import Compose from './components/compose';
|
|
|
|
import Icon from './components/icon';
|
|
|
|
import Loader from './components/loader';
|
|
|
|
import Modal from './components/modal';
|
|
|
|
import Home from './pages/home';
|
|
|
|
import Login from './pages/login';
|
|
|
|
import Notifications from './pages/notifications';
|
|
|
|
import Settings from './pages/settings';
|
|
|
|
import Status from './pages/status';
|
|
|
|
import Welcome from './pages/welcome';
|
|
|
|
import { getAccessToken } from './utils/auth';
|
2022-12-13 16:54:16 +03:00
|
|
|
import openCompose from './utils/open-compose';
|
2022-12-10 12:14:48 +03:00
|
|
|
import states from './utils/states';
|
|
|
|
import store from './utils/store';
|
|
|
|
|
|
|
|
const { VITE_CLIENT_NAME: CLIENT_NAME } = import.meta.env;
|
|
|
|
|
2022-12-13 15:42:09 +03:00
|
|
|
window.__STATES__ = states;
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
|
|
async function startStream() {
|
2022-12-25 18:28:55 +03:00
|
|
|
const stream = await masto.v1.stream.streamUser();
|
2022-12-10 12:14:48 +03:00
|
|
|
console.log('STREAM START', { stream });
|
|
|
|
stream.on('update', (status) => {
|
|
|
|
console.log('UPDATE', status);
|
|
|
|
|
|
|
|
const inHomeNew = states.homeNew.find((s) => s.id === status.id);
|
|
|
|
const inHome = states.home.find((s) => s.id === status.id);
|
|
|
|
if (!inHomeNew && !inHome) {
|
|
|
|
states.homeNew.unshift({
|
|
|
|
id: status.id,
|
|
|
|
reblog: status.reblog?.id,
|
|
|
|
reply: !!status.inReplyToAccountId,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
states.statuses.set(status.id, status);
|
|
|
|
if (status.reblog) {
|
|
|
|
states.statuses.set(status.reblog.id, status.reblog);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
stream.on('status.update', (status) => {
|
|
|
|
console.log('STATUS.UPDATE', status);
|
|
|
|
states.statuses.set(status.id, status);
|
|
|
|
if (status.reblog) {
|
|
|
|
states.statuses.set(status.reblog.id, status.reblog);
|
|
|
|
}
|
|
|
|
});
|
2022-12-15 05:59:20 +03:00
|
|
|
stream.on('delete', (statusID) => {
|
|
|
|
console.log('DELETE', statusID);
|
2022-12-22 17:43:04 +03:00
|
|
|
// states.statuses.delete(statusID);
|
|
|
|
const s = states.statuses.get(statusID);
|
|
|
|
if (s) s._deleted = true;
|
2022-12-15 05:59:20 +03:00
|
|
|
});
|
2022-12-10 12:14:48 +03:00
|
|
|
stream.on('notification', (notification) => {
|
|
|
|
console.log('NOTIFICATION', notification);
|
|
|
|
|
|
|
|
const inNotificationsNew = states.notificationsNew.find(
|
|
|
|
(n) => n.id === notification.id,
|
|
|
|
);
|
|
|
|
const inNotifications = states.notifications.find(
|
|
|
|
(n) => n.id === notification.id,
|
|
|
|
);
|
|
|
|
if (!inNotificationsNew && !inNotifications) {
|
|
|
|
states.notificationsNew.unshift(notification);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (notification.status && !states.statuses.has(notification.status.id)) {
|
|
|
|
states.statuses.set(notification.status.id, notification.status);
|
|
|
|
if (
|
|
|
|
notification.status.reblog &&
|
|
|
|
!states.statuses.has(notification.status.reblog.id)
|
|
|
|
) {
|
|
|
|
states.statuses.set(
|
|
|
|
notification.status.reblog.id,
|
|
|
|
notification.status.reblog,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
stream.ws.onclose = () => {
|
|
|
|
console.log('STREAM CLOSED!');
|
|
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
startStream();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
stream,
|
|
|
|
stopStream: () => {
|
|
|
|
stream.ws.close();
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-12-24 05:26:05 +03:00
|
|
|
function startVisibility() {
|
|
|
|
const handleVisibilityChange = () => {
|
2022-12-26 12:39:20 +03:00
|
|
|
if (document.visibilityState === 'hidden') {
|
2022-12-24 05:26:05 +03:00
|
|
|
const timestamp = Date.now();
|
|
|
|
store.session.set('lastHidden', timestamp);
|
|
|
|
} else {
|
|
|
|
const timestamp = Date.now();
|
|
|
|
const lastHidden = store.session.get('lastHidden');
|
|
|
|
const diff = timestamp - lastHidden;
|
|
|
|
const diffMins = Math.round(diff / 1000 / 60);
|
|
|
|
if (diffMins > 1) {
|
|
|
|
console.log('visible', { lastHidden, diffMins });
|
|
|
|
setTimeout(() => {
|
|
|
|
// Buffer for WS reconnect
|
|
|
|
(async () => {
|
|
|
|
try {
|
2022-12-25 18:28:55 +03:00
|
|
|
const fetchHome = masto.v1.timelines.listHome({
|
2022-12-25 20:09:53 +03:00
|
|
|
limit: 1,
|
2022-12-24 05:26:05 +03:00
|
|
|
});
|
2022-12-25 18:28:55 +03:00
|
|
|
const fetchNotifications = masto.v1.notifications.list({
|
|
|
|
limit: 1,
|
|
|
|
});
|
2022-12-24 05:26:05 +03:00
|
|
|
|
|
|
|
const newStatuses = await fetchHome;
|
|
|
|
if (
|
2022-12-25 18:28:55 +03:00
|
|
|
newStatuses.length &&
|
|
|
|
newStatuses[0].id !== states.home[0].id
|
2022-12-24 05:26:05 +03:00
|
|
|
) {
|
2022-12-25 18:28:55 +03:00
|
|
|
states.homeNew = newStatuses.map((status) => {
|
2022-12-24 05:26:05 +03:00
|
|
|
states.statuses.set(status.id, status);
|
|
|
|
if (status.reblog) {
|
|
|
|
states.statuses.set(status.reblog.id, status.reblog);
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
id: status.id,
|
|
|
|
reblog: status.reblog?.id,
|
|
|
|
reply: !!status.inReplyToAccountId,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const newNotifications = await fetchNotifications;
|
2022-12-25 18:28:55 +03:00
|
|
|
if (newNotifications.length) {
|
|
|
|
const notification = newNotifications[0];
|
2022-12-24 05:26:05 +03:00
|
|
|
const inNotificationsNew = states.notificationsNew.find(
|
|
|
|
(n) => n.id === notification.id,
|
|
|
|
);
|
|
|
|
const inNotifications = states.notifications.find(
|
|
|
|
(n) => n.id === notification.id,
|
|
|
|
);
|
|
|
|
if (!inNotificationsNew && !inNotifications) {
|
|
|
|
states.notificationsNew.unshift(notification);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
notification.status &&
|
|
|
|
!states.statuses.has(notification.status.id)
|
|
|
|
) {
|
|
|
|
states.statuses.set(
|
|
|
|
notification.status.id,
|
|
|
|
notification.status,
|
|
|
|
);
|
|
|
|
if (
|
|
|
|
notification.status.reblog &&
|
|
|
|
!states.statuses.has(notification.status.reblog.id)
|
|
|
|
) {
|
|
|
|
states.statuses.set(
|
|
|
|
notification.status.reblog.id,
|
|
|
|
notification.status.reblog,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
// Silently fail
|
|
|
|
console.error(e);
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
}, 100);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
|
|
return {
|
|
|
|
stop: () => {
|
|
|
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-12-10 12:14:48 +03:00
|
|
|
export function App() {
|
|
|
|
const snapStates = useSnapshot(states);
|
|
|
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
2022-12-21 15:34:24 +03:00
|
|
|
const [uiState, setUIState] = useState('loading');
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
|
|
useLayoutEffect(() => {
|
|
|
|
const theme = store.local.get('theme');
|
|
|
|
if (theme) {
|
|
|
|
document.documentElement.classList.add(`is-${theme}`);
|
|
|
|
document
|
|
|
|
.querySelector('meta[name="color-scheme"]')
|
|
|
|
.setAttribute('content', theme);
|
|
|
|
}
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const instanceURL = store.local.get('instanceURL');
|
|
|
|
const accounts = store.local.getJSON('accounts') || [];
|
|
|
|
const code = (window.location.search.match(/code=([^&]+)/) || [])[1];
|
|
|
|
|
|
|
|
if (code) {
|
|
|
|
console.log({ code });
|
|
|
|
// Clear the code from the URL
|
|
|
|
window.history.replaceState({}, document.title, '/');
|
|
|
|
|
|
|
|
const clientID = store.session.get('clientID');
|
|
|
|
const clientSecret = store.session.get('clientSecret');
|
|
|
|
|
|
|
|
(async () => {
|
|
|
|
setUIState('loading');
|
|
|
|
const tokenJSON = await getAccessToken({
|
|
|
|
instanceURL,
|
|
|
|
client_id: clientID,
|
|
|
|
client_secret: clientSecret,
|
|
|
|
code,
|
|
|
|
});
|
|
|
|
const { access_token: accessToken } = tokenJSON;
|
|
|
|
store.session.set('accessToken', accessToken);
|
|
|
|
|
|
|
|
window.masto = await login({
|
|
|
|
url: `https://${instanceURL}`,
|
|
|
|
accessToken,
|
2022-12-15 11:54:14 +03:00
|
|
|
disableVersionCheck: true,
|
2022-12-20 08:29:38 +03:00
|
|
|
timeout: 30_000,
|
2022-12-10 12:14:48 +03:00
|
|
|
});
|
|
|
|
|
2022-12-25 18:28:55 +03:00
|
|
|
const mastoAccount = await masto.v1.accounts.verifyCredentials();
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
|
|
console.log({ tokenJSON, mastoAccount });
|
|
|
|
|
|
|
|
let account = accounts.find((a) => a.info.id === mastoAccount.id);
|
|
|
|
if (account) {
|
|
|
|
account.info = mastoAccount;
|
|
|
|
account.instanceURL = instanceURL;
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
|
|
|
|
(async () => {
|
|
|
|
try {
|
|
|
|
setUIState('loading');
|
|
|
|
window.masto = await login({
|
|
|
|
url: `https://${instanceURL}`,
|
|
|
|
accessToken,
|
2022-12-15 11:54:14 +03:00
|
|
|
disableVersionCheck: true,
|
2022-12-20 08:29:38 +03:00
|
|
|
timeout: 30_000,
|
2022-12-10 12:14:48 +03:00
|
|
|
});
|
|
|
|
setIsLoggedIn(true);
|
|
|
|
} catch (e) {
|
|
|
|
setIsLoggedIn(false);
|
|
|
|
}
|
|
|
|
setUIState('default');
|
|
|
|
})();
|
2022-12-21 15:34:24 +03:00
|
|
|
} else {
|
|
|
|
setUIState('default');
|
2022-12-10 12:14:48 +03:00
|
|
|
}
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const [currentDeck, setCurrentDeck] = useState('home');
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
// HACK: prevent this from running again due to HMR
|
|
|
|
if (states.init) return;
|
|
|
|
|
|
|
|
if (isLoggedIn) {
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
startStream();
|
2022-12-24 05:26:05 +03:00
|
|
|
startVisibility();
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
|
|
// Collect instance info
|
|
|
|
(async () => {
|
2022-12-25 18:28:55 +03:00
|
|
|
const info = await masto.v2.instance.fetch();
|
2022-12-10 12:14:48 +03:00
|
|
|
console.log(info);
|
|
|
|
const { uri } = info;
|
|
|
|
const instances = store.local.getJSON('instances') || {};
|
|
|
|
instances[uri] = info;
|
|
|
|
store.local.setJSON('instances', instances);
|
|
|
|
})();
|
|
|
|
});
|
|
|
|
states.init = true;
|
|
|
|
}
|
|
|
|
}, [isLoggedIn]);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{isLoggedIn && currentDeck && (
|
|
|
|
<>
|
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
id="compose-button"
|
2022-12-13 16:54:16 +03:00
|
|
|
onClick={(e) => {
|
|
|
|
if (e.shiftKey) {
|
|
|
|
const newWin = openCompose();
|
|
|
|
if (!newWin) {
|
|
|
|
alert('Looks like your browser is blocking popups.');
|
|
|
|
states.showCompose = true;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
states.showCompose = true;
|
|
|
|
}
|
|
|
|
}}
|
2022-12-10 12:14:48 +03:00
|
|
|
>
|
|
|
|
<Icon icon="quill" size="xxl" alt="Compose" />
|
|
|
|
</button>
|
|
|
|
<div class="decks">
|
|
|
|
{/* Home will never be unmounted */}
|
|
|
|
<Home hidden={currentDeck !== 'home'} />
|
|
|
|
{/* Notifications can be unmounted */}
|
|
|
|
{currentDeck === 'notifications' && <Notifications />}
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
{!isLoggedIn && uiState === 'loading' && <Loader />}
|
|
|
|
<Router
|
|
|
|
history={createHashHistory()}
|
|
|
|
onChange={(e) => {
|
|
|
|
// Special handling for Home and Notifications
|
|
|
|
const { url } = e;
|
|
|
|
if (/notifications/i.test(url)) {
|
|
|
|
setCurrentDeck('notifications');
|
|
|
|
} else if (url === '/') {
|
|
|
|
setCurrentDeck('home');
|
2022-12-10 16:02:03 +03:00
|
|
|
document.title = `Home / ${CLIENT_NAME}`;
|
2022-12-10 12:14:48 +03:00
|
|
|
} else if (url === '/login' || url === '/welcome') {
|
|
|
|
setCurrentDeck(null);
|
|
|
|
}
|
|
|
|
states.history.push(url);
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{!isLoggedIn && uiState !== 'loading' && <Welcome path="/" />}
|
|
|
|
<Welcome path="/welcome" />
|
|
|
|
{isLoggedIn && <Status path="/s/:id" />}
|
|
|
|
<Login path="/login" />
|
|
|
|
</Router>
|
|
|
|
{!!snapStates.showCompose && (
|
|
|
|
<Modal>
|
|
|
|
<Compose
|
|
|
|
replyToStatus={
|
|
|
|
typeof snapStates.showCompose !== 'boolean'
|
|
|
|
? snapStates.showCompose.replyToStatus
|
|
|
|
: null
|
|
|
|
}
|
2022-12-14 16:48:17 +03:00
|
|
|
editStatus={states.showCompose?.editStatus || null}
|
|
|
|
draftStatus={states.showCompose?.draftStatus || null}
|
2022-12-13 16:54:16 +03:00
|
|
|
onClose={(results) => {
|
|
|
|
const { newStatus } = results || {};
|
2022-12-10 12:14:48 +03:00
|
|
|
states.showCompose = false;
|
2022-12-13 16:54:16 +03:00
|
|
|
if (newStatus) {
|
2022-12-10 12:14:48 +03:00
|
|
|
states.reloadStatusPage++;
|
2022-12-26 09:02:05 +03:00
|
|
|
const toast = Toastify({
|
|
|
|
text: 'Status posted. Check it out.',
|
|
|
|
duration: 10_000, // 10 seconds
|
|
|
|
gravity: 'bottom',
|
|
|
|
position: 'center',
|
|
|
|
// destination: `/#/s/${newStatus.id}`,
|
|
|
|
onClick: () => {
|
|
|
|
toast.hideToast();
|
|
|
|
route(`/s/${newStatus.id}`);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
toast.showToast();
|
2022-12-10 12:14:48 +03:00
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</Modal>
|
|
|
|
)}
|
|
|
|
{!!snapStates.showSettings && (
|
|
|
|
<Modal
|
|
|
|
onClick={(e) => {
|
|
|
|
if (e.target === e.currentTarget) {
|
|
|
|
states.showSettings = false;
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Settings
|
|
|
|
onClose={() => {
|
|
|
|
states.showSettings = false;
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</Modal>
|
|
|
|
)}
|
|
|
|
{!!snapStates.showAccount && (
|
|
|
|
<Modal
|
|
|
|
class="light"
|
|
|
|
onClick={(e) => {
|
|
|
|
if (e.target === e.currentTarget) {
|
|
|
|
states.showAccount = false;
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Account account={snapStates.showAccount} />
|
|
|
|
</Modal>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|