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