this repo has no description
1import { proxy, subscribe } from 'valtio';
2import { subscribeKey } from 'valtio/utils';
3
4import { api } from './api';
5import isMastodonLinkMaybe from './isMastodonLinkMaybe';
6import pmem from './pmem';
7import rateLimit from './ratelimit';
8import store from './store';
9import unfurlMastodonLink from './unfurl-link';
10
11const states = proxy({
12 appVersion: {},
13 // history: [],
14 prevLocation: null,
15 currentLocation: null,
16 statuses: {},
17 statusThreadNumber: {},
18 home: [],
19 // specialHome: [],
20 homeNew: [],
21 homeLast: null, // Last item in 'home' list
22 homeLastFetchTime: null,
23 notifications: [],
24 notificationsLast: null, // Last read notification
25 notificationsNew: [],
26 notificationsShowNew: false,
27 notificationsLastFetchTime: null,
28 reloadStatusPage: 0,
29 reloadGenericAccounts: {
30 id: null,
31 counter: 0,
32 },
33 spoilers: {},
34 spoilersMedia: {},
35 scrollPositions: {},
36 unfurledLinks: {},
37 statusQuotes: {},
38 statusFollowedTags: {},
39 accounts: {},
40 routeNotification: null,
41 // Modals
42 showCompose: false,
43 showSettings: false,
44 showAccount: false,
45 showAccounts: false,
46 showDrafts: false,
47 showMediaModal: false,
48 showShortcutsSettings: false,
49 showKeyboardShortcutsHelp: false,
50 showGenericAccounts: false,
51 showMediaAlt: false,
52 // Shortcuts
53 shortcuts: [],
54 // Settings
55 settings: {
56 autoRefresh: false,
57 shortcutsViewMode: null,
58 shortcutsColumnsMode: false,
59 boostsCarousel: true,
60 contentTranslation: true,
61 contentTranslationTargetLanguage: null,
62 contentTranslationHideLanguages: [],
63 contentTranslationAutoInline: false,
64 mediaAltGenerator: false,
65 cloakMode: false,
66 },
67});
68
69export default states;
70
71export function initStates() {
72 // init all account based states
73 // all keys that uses store.account.get() should be initialized here
74 states.notificationsLast = store.account.get('notificationsLast') || null;
75 states.shortcuts = store.account.get('shortcuts') ?? [];
76 states.settings.autoRefresh =
77 store.account.get('settings-autoRefresh') ?? false;
78 states.settings.shortcutsViewMode =
79 store.account.get('settings-shortcutsViewMode') ?? null;
80 if (store.account.get('settings-shortcutsColumnsMode')) {
81 states.settings.shortcutsColumnsMode = true;
82 }
83 states.settings.boostsCarousel =
84 store.account.get('settings-boostsCarousel') ?? true;
85 states.settings.contentTranslation =
86 store.account.get('settings-contentTranslation') ?? true;
87 states.settings.contentTranslationTargetLanguage =
88 store.account.get('settings-contentTranslationTargetLanguage') || null;
89 states.settings.contentTranslationHideLanguages =
90 store.account.get('settings-contentTranslationHideLanguages') || [];
91 states.settings.contentTranslationAutoInline =
92 store.account.get('settings-contentTranslationAutoInline') ?? false;
93 states.settings.mediaAltGenerator =
94 store.account.get('settings-mediaAltGenerator') ?? false;
95 states.settings.cloakMode = store.account.get('settings-cloakMode') ?? false;
96}
97
98subscribeKey(states, 'notificationsLast', (v) => {
99 console.log('CHANGE', v);
100 store.account.set('notificationsLast', states.notificationsLast);
101});
102subscribe(states, (changes) => {
103 console.debug('STATES change', changes);
104 for (const [action, path, value, prevValue] of changes) {
105 if (path.join('.') === 'settings.autoRefresh') {
106 store.account.set('settings-autoRefresh', !!value);
107 }
108 if (path.join('.') === 'settings.boostsCarousel') {
109 store.account.set('settings-boostsCarousel', !!value);
110 }
111 if (path.join('.') === 'settings.shortcutsViewMode') {
112 store.account.set('settings-shortcutsViewMode', value);
113 }
114 if (path.join('.') === 'settings.contentTranslation') {
115 store.account.set('settings-contentTranslation', !!value);
116 }
117 if (path.join('.') === 'settings.contentTranslationAutoInline') {
118 store.account.set('settings-contentTranslationAutoInline', !!value);
119 }
120 if (path.join('.') === 'settings.contentTranslationTargetLanguage') {
121 console.log('SET', value);
122 store.account.set('settings-contentTranslationTargetLanguage', value);
123 }
124 if (/^settings\.contentTranslationHideLanguages/i.test(path.join('.'))) {
125 store.account.set(
126 'settings-contentTranslationHideLanguages',
127 states.settings.contentTranslationHideLanguages,
128 );
129 }
130 if (path.join('.') === 'settings.mediaAltGenerator') {
131 store.account.set('settings-mediaAltGenerator', !!value);
132 }
133 if (path?.[0] === 'shortcuts') {
134 store.account.set('shortcuts', states.shortcuts);
135 }
136 if (path.join('.') === 'settings.cloakMode') {
137 store.account.set('settings-cloakMode', !!value);
138 }
139 }
140});
141
142export function hideAllModals() {
143 states.showCompose = false;
144 states.showSettings = false;
145 states.showAccount = false;
146 states.showAccounts = false;
147 states.showDrafts = false;
148 states.showMediaModal = false;
149 states.showShortcutsSettings = false;
150 states.showKeyboardShortcutsHelp = false;
151 states.showGenericAccounts = false;
152 states.showMediaAlt = false;
153}
154
155export function statusKey(id, instance) {
156 if (!id) return;
157 return instance ? `${instance}/${id}` : id;
158}
159
160export function getStatus(statusID, instance) {
161 if (instance) {
162 const key = statusKey(statusID, instance);
163 return states.statuses[key];
164 }
165 return states.statuses[statusID];
166}
167
168export function saveStatus(status, instance, opts) {
169 if (typeof instance === 'object') {
170 opts = instance;
171 instance = null;
172 }
173 const {
174 override = true,
175 skipThreading = false,
176 skipUnfurling = false,
177 } = opts || {};
178 if (!status) return;
179 const oldStatus = getStatus(status.id, instance);
180 if (!override && oldStatus) return;
181 queueMicrotask(() => {
182 const key = statusKey(status.id, instance);
183 if (oldStatus?._pinned) status._pinned = oldStatus._pinned;
184 // if (oldStatus?._filtered) status._filtered = oldStatus._filtered;
185 states.statuses[key] = status;
186 if (status.reblog) {
187 const key = statusKey(status.reblog.id, instance);
188 states.statuses[key] = status.reblog;
189 }
190 });
191
192 // THREAD TRAVERSER
193 if (!skipThreading) {
194 queueMicrotask(() => {
195 threadifyStatus(status.reblog || status, instance);
196 });
197 }
198
199 // UNFURLER
200 if (!skipUnfurling) {
201 queueMicrotask(() => {
202 unfurlStatus(status.reblog || status, instance);
203 });
204 }
205}
206
207function _threadifyStatus(status, propInstance) {
208 const { masto, instance } = api({ instance: propInstance });
209 // Return all statuses in the thread, via inReplyToId, if inReplyToAccountId === account.id
210 let fetchIndex = 0;
211 async function traverse(status, index = 0) {
212 const { inReplyToId, inReplyToAccountId } = status;
213 if (!inReplyToId || inReplyToAccountId !== status.account.id) {
214 return [status];
215 }
216 if (inReplyToId && inReplyToAccountId !== status.account.id) {
217 throw 'Not a thread';
218 // Possibly thread of replies by multiple people?
219 }
220 const key = statusKey(inReplyToId, instance);
221 let prevStatus = states.statuses[key];
222 if (!prevStatus) {
223 if (fetchIndex++ > 3) throw 'Too many fetches for thread'; // Some people revive old threads
224 await new Promise((r) => setTimeout(r, 500 * fetchIndex)); // Be nice to rate limits
225 // prevStatus = await masto.v1.statuses.$.select(inReplyToId).fetch();
226 prevStatus = await fetchStatus(inReplyToId, masto);
227 saveStatus(prevStatus, instance, { skipThreading: true });
228 }
229 // Prepend so that first status in thread will be index 0
230 return [...(await traverse(prevStatus, ++index)), status];
231 }
232 return traverse(status)
233 .then((statuses) => {
234 if (statuses.length > 1) {
235 console.debug('THREAD', statuses);
236 statuses.forEach((status, index) => {
237 const key = statusKey(status.id, instance);
238 states.statusThreadNumber[key] = index + 1;
239 });
240 }
241 })
242 .catch((e) => {
243 console.error(e, status);
244 });
245}
246export const threadifyStatus = rateLimit(_threadifyStatus, 100);
247
248const fauxDiv = document.createElement('div');
249export function unfurlStatus(status, instance) {
250 const { instance: currentInstance } = api();
251 const content = status?.content;
252 const hasLink = /<a/i.test(content);
253 if (hasLink) {
254 const sKey = statusKey(status?.id, instance);
255 fauxDiv.innerHTML = content;
256 const links = fauxDiv.querySelectorAll(
257 'a[href]:not(.u-url):not(.mention):not(.hashtag)',
258 );
259 [...links]
260 .filter((a) => {
261 const url = a.href;
262 const isPostItself = url === status.url || url === status.uri;
263 return !isPostItself && isMastodonLinkMaybe(url);
264 })
265 .forEach((a, i) => {
266 unfurlMastodonLink(currentInstance, a.href).then((result) => {
267 if (!result) return;
268 if (!sKey) return;
269 if (!Array.isArray(states.statusQuotes[sKey])) {
270 states.statusQuotes[sKey] = [];
271 }
272 if (!states.statusQuotes[sKey][i]) {
273 states.statusQuotes[sKey].splice(i, 0, result);
274 }
275 });
276 });
277 }
278}
279
280const fetchStatus = pmem((statusID, masto) => {
281 return masto.v1.statuses.$select(statusID).fetch();
282});