phanpy/src/utils/states.js

329 lines
11 KiB
JavaScript
Raw Normal View History

2024-01-06 12:31:25 +08:00
import { deepEqual } from 'fast-equals';
2023-02-16 17:51:54 +08:00
import { proxy, subscribe } from 'valtio';
import { subscribeKey } from 'valtio/utils';
2023-01-14 19:42:04 +08:00
import { api } from './api';
import isMastodonLinkMaybe from './isMastodonLinkMaybe';
2023-10-14 20:33:40 +08:00
import pmem from './pmem';
import rateLimit from './ratelimit';
2023-01-14 19:42:04 +08:00
import store from './store';
import unfurlMastodonLink from './unfurl-link';
2022-12-10 17:14:48 +08:00
2023-01-09 19:11:34 +08:00
const states = proxy({
2023-02-28 15:27:42 +08:00
appVersion: {},
// history: [],
prevLocation: null,
currentLocation: null,
statuses: {},
2023-01-10 19:59:02 +08:00
statusThreadNumber: {},
2022-12-10 17:14:48 +08:00
home: [],
// specialHome: [],
2022-12-10 17:14:48 +08:00
homeNew: [],
homeLast: null, // Last item in 'home' list
2022-12-10 17:14:48 +08:00
homeLastFetchTime: null,
notifications: [],
notificationsLast: null, // Last read notification
2022-12-10 17:14:48 +08:00
notificationsNew: [],
2023-02-12 17:38:50 +08:00
notificationsShowNew: false,
2022-12-10 17:14:48 +08:00
notificationsLastFetchTime: null,
reloadStatusPage: 0,
reloadGenericAccounts: {
id: null,
counter: 0,
},
reloadScheduledPosts: 0,
spoilers: {},
spoilersMedia: {},
scrollPositions: {},
unfurledLinks: {},
2023-04-23 00:55:47 +08:00
statusQuotes: {},
2023-12-15 01:58:29 +08:00
statusFollowedTags: {},
2024-01-30 14:34:54 +08:00
statusReply: {},
accounts: {},
routeNotification: null,
2024-05-24 12:30:20 +08:00
composerState: {},
2022-12-10 17:14:48 +08:00
// Modals
showCompose: false,
showSettings: false,
showAccount: false,
showAccounts: false,
showDrafts: false,
showMediaModal: false,
2023-02-16 17:51:54 +08:00
showShortcutsSettings: false,
2023-09-06 22:54:05 +08:00
showKeyboardShortcutsHelp: false,
2023-09-28 11:22:05 +08:00
showGenericAccounts: false,
2023-09-28 15:48:32 +08:00
showMediaAlt: false,
2024-01-06 16:46:45 +08:00
showEmbedModal: false,
2024-02-26 13:59:26 +08:00
showReportModal: false,
2023-02-16 17:51:54 +08:00
// Shortcuts
shortcuts: [],
// Settings
2023-01-14 19:42:04 +08:00
settings: {
autoRefresh: false,
shortcutsViewMode: null,
shortcutsColumnsMode: false,
boostsCarousel: true,
contentTranslation: true,
contentTranslationTargetLanguage: null,
contentTranslationHideLanguages: [],
contentTranslationAutoInline: false,
shortcutSettingsCloudImportExport: false,
mediaAltGenerator: false,
2024-04-02 17:51:48 +08:00
composerGIFPicker: false,
cloakMode: false,
groupedNotificationsAlpha: false,
2023-01-14 19:42:04 +08:00
},
2022-12-10 17:14:48 +08:00
});
2023-01-09 19:11:34 +08:00
export default states;
export function initStates() {
// init all account based states
// all keys that uses store.account.get() should be initialized here
states.notificationsLast = store.account.get('notificationsLast') || null;
states.shortcuts = store.account.get('shortcuts') ?? [];
states.settings.autoRefresh =
store.account.get('settings-autoRefresh') ?? false;
states.settings.shortcutsViewMode =
store.account.get('settings-shortcutsViewMode') ?? null;
if (store.account.get('settings-shortcutsColumnsMode')) {
states.settings.shortcutsColumnsMode = true;
}
states.settings.boostsCarousel =
store.account.get('settings-boostsCarousel') ?? true;
states.settings.contentTranslation =
store.account.get('settings-contentTranslation') ?? true;
states.settings.contentTranslationTargetLanguage =
store.account.get('settings-contentTranslationTargetLanguage') || null;
states.settings.contentTranslationHideLanguages =
store.account.get('settings-contentTranslationHideLanguages') || [];
states.settings.contentTranslationAutoInline =
store.account.get('settings-contentTranslationAutoInline') ?? false;
states.settings.shortcutSettingsCloudImportExport =
store.account.get('settings-shortcutSettingsCloudImportExport') ?? false;
states.settings.mediaAltGenerator =
store.account.get('settings-mediaAltGenerator') ?? false;
2024-04-02 17:51:48 +08:00
states.settings.composerGIFPicker =
store.account.get('settings-composerGIFPicker') ?? false;
states.settings.cloakMode = store.account.get('settings-cloakMode') ?? false;
states.settings.groupedNotificationsAlpha =
store.account.get('settings-groupedNotificationsAlpha') ?? false;
}
subscribeKey(states, 'notificationsLast', (v) => {
console.log('CHANGE', v);
store.account.set('notificationsLast', states.notificationsLast);
});
subscribe(states, (changes) => {
console.debug('STATES change', changes);
for (const [action, path, value, prevValue] of changes) {
2023-05-05 17:53:16 +08:00
if (path.join('.') === 'settings.autoRefresh') {
store.account.set('settings-autoRefresh', !!value);
}
if (path.join('.') === 'settings.boostsCarousel') {
store.account.set('settings-boostsCarousel', !!value);
}
if (path.join('.') === 'settings.shortcutsViewMode') {
store.account.set('settings-shortcutsViewMode', value);
}
if (path.join('.') === 'settings.contentTranslation') {
store.account.set('settings-contentTranslation', !!value);
}
if (path.join('.') === 'settings.contentTranslationAutoInline') {
store.account.set('settings-contentTranslationAutoInline', !!value);
}
if (path.join('.') === 'settings.shortcutSettingsCloudImportExport') {
store.account.set('settings-shortcutSettingsCloudImportExport', !!value);
}
if (path.join('.') === 'settings.contentTranslationTargetLanguage') {
console.log('SET', value);
store.account.set('settings-contentTranslationTargetLanguage', value);
}
if (/^settings\.contentTranslationHideLanguages/i.test(path.join('.'))) {
store.account.set(
'settings-contentTranslationHideLanguages',
states.settings.contentTranslationHideLanguages,
);
}
if (path.join('.') === 'settings.mediaAltGenerator') {
store.account.set('settings-mediaAltGenerator', !!value);
}
2024-04-02 17:51:48 +08:00
if (path.join('.') === 'settings.composerGIFPicker') {
store.account.set('settings-composerGIFPicker', !!value);
}
if (path?.[0] === 'shortcuts') {
store.account.set('shortcuts', states.shortcuts);
}
2023-04-23 12:08:41 +08:00
if (path.join('.') === 'settings.cloakMode') {
store.account.set('settings-cloakMode', !!value);
}
if (path.join('.') === 'settings.groupedNotificationsAlpha') {
store.account.set('settings-groupedNotificationsAlpha', !!value);
}
2023-02-16 17:51:54 +08:00
}
});
2023-01-14 19:42:04 +08:00
2023-02-02 10:30:16 +08:00
export function hideAllModals() {
states.showCompose = false;
states.showSettings = false;
states.showAccount = false;
states.showAccounts = false;
2023-02-02 10:30:16 +08:00
states.showDrafts = false;
states.showMediaModal = false;
2023-02-17 11:29:53 +08:00
states.showShortcutsSettings = false;
2023-09-06 22:54:05 +08:00
states.showKeyboardShortcutsHelp = false;
2023-09-17 12:54:48 +08:00
states.showGenericAccounts = false;
2023-09-28 15:48:32 +08:00
states.showMediaAlt = false;
2024-01-06 16:46:45 +08:00
states.showEmbedModal = false;
2023-02-02 10:30:16 +08:00
}
export function statusKey(id, instance) {
2023-05-20 01:06:16 +08:00
if (!id) return;
return instance ? `${instance}/${id}` : id;
}
export function getStatus(statusID, instance) {
if (instance) {
const key = statusKey(statusID, instance);
return states.statuses[key];
}
return states.statuses[statusID];
}
export function saveStatus(status, instance, opts) {
if (typeof instance === 'object') {
opts = instance;
instance = null;
}
const {
override = true,
skipThreading = false,
skipUnfurling = false,
} = opts || {};
2023-01-09 19:11:34 +08:00
if (!status) return;
2023-03-22 00:09:36 +08:00
const oldStatus = getStatus(status.id, instance);
if (!override && oldStatus) return;
2024-01-06 12:31:25 +08:00
if (deepEqual(status, oldStatus)) return;
2023-12-29 08:25:58 +08:00
queueMicrotask(() => {
const key = statusKey(status.id, instance);
if (oldStatus?._pinned) status._pinned = oldStatus._pinned;
// if (oldStatus?._filtered) status._filtered = oldStatus._filtered;
states.statuses[key] = status;
2024-01-25 12:59:53 +08:00
if (status.reblog?.id) {
const srKey = statusKey(status.reblog.id, instance);
states.statuses[srKey] = status.reblog;
}
if (status.quote?.id) {
const sKey = statusKey(status.quote.id, instance);
states.statuses[sKey] = status.quote;
states.statusQuotes[key] = [
{
id: status.quote.id,
instance,
},
];
2023-12-29 08:25:58 +08:00
}
});
2023-01-10 19:59:02 +08:00
// THREAD TRAVERSER
if (!skipThreading) {
2023-12-27 23:32:52 +08:00
queueMicrotask(() => {
threadifyStatus(status.reblog || status, instance);
2023-01-10 19:59:02 +08:00
});
}
// UNFURLER
if (!skipUnfurling) {
queueMicrotask(() => {
unfurlStatus(status.reblog || status, instance);
});
}
2023-01-10 19:59:02 +08:00
}
function _threadifyStatus(status, propInstance) {
const { masto, instance } = api({ instance: propInstance });
2023-01-10 19:59:02 +08:00
// Return all statuses in the thread, via inReplyToId, if inReplyToAccountId === account.id
let fetchIndex = 0;
async function traverse(status, index = 0) {
const { inReplyToId, inReplyToAccountId } = status;
if (!inReplyToId || inReplyToAccountId !== status.account.id) {
return [status];
}
if (inReplyToId && inReplyToAccountId !== status.account.id) {
throw 'Not a thread';
// Possibly thread of replies by multiple people?
}
const key = statusKey(inReplyToId, instance);
let prevStatus = states.statuses[key];
2023-01-10 19:59:02 +08:00
if (!prevStatus) {
if (fetchIndex++ > 3) throw 'Too many fetches for thread'; // Some people revive old threads
2023-12-22 10:19:06 +08:00
await new Promise((r) => setTimeout(r, 500 * fetchIndex)); // Be nice to rate limits
// prevStatus = await masto.v1.statuses.$.select(inReplyToId).fetch();
2023-02-23 22:53:12 +08:00
prevStatus = await fetchStatus(inReplyToId, masto);
saveStatus(prevStatus, instance, { skipThreading: true });
2023-01-10 19:59:02 +08:00
}
// Prepend so that first status in thread will be index 0
return [...(await traverse(prevStatus, ++index)), status];
}
return traverse(status)
.then((statuses) => {
if (statuses.length > 1) {
console.debug('THREAD', statuses);
statuses.forEach((status, index) => {
const key = statusKey(status.id, instance);
states.statusThreadNumber[key] = index + 1;
2023-01-10 19:59:02 +08:00
});
}
})
.catch((e) => {
console.error(e, status);
});
2023-01-09 19:11:34 +08:00
}
2023-12-22 10:19:06 +08:00
export const threadifyStatus = rateLimit(_threadifyStatus, 100);
2023-02-23 22:53:12 +08:00
const fauxDiv = document.createElement('div');
export function unfurlStatus(status, instance) {
const { instance: currentInstance } = api();
const content = status?.content;
const hasLink = /<a/i.test(content);
if (hasLink) {
const sKey = statusKey(status?.id, instance);
fauxDiv.innerHTML = content;
const links = fauxDiv.querySelectorAll(
'a[href]:not(.u-url):not(.mention):not(.hashtag)',
);
[...links]
.filter((a) => {
const url = a.href;
const isPostItself = url === status.url || url === status.uri;
return !isPostItself && isMastodonLinkMaybe(url);
})
.forEach((a, i) => {
unfurlMastodonLink(currentInstance, a.href).then((result) => {
if (!result) return;
if (!sKey) return;
2024-07-12 13:34:57 +08:00
if (result?.id === status.id) {
// Unfurled post is the post itself???
// Scenario:
// 1. Post with [URL]
// 2. Unfurl [URL], API returns the same post that contains [URL]
// 3. 💥 Recursive quote posts 💥
// Note: Mastodon search doesn't return posts that contains [URL], it's actually used to *resolve* the URL
// But some non-Mastodon servers, their search API will eventually search posts that contains [URL] and return them
return;
}
if (!Array.isArray(states.statusQuotes[sKey])) {
states.statusQuotes[sKey] = [];
}
if (!states.statusQuotes[sKey][i]) {
states.statusQuotes[sKey].splice(i, 0, result);
}
});
});
}
}
2023-10-14 20:33:40 +08:00
const fetchStatus = pmem((statusID, masto) => {
return masto.v1.statuses.$select(statusID).fetch();
2023-02-23 22:53:12 +08:00
});