this repo has no description
at hotfix/infinite-loop-intersection-observer 282 lines 9.2 kB view raw
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});