/ src / utils / states.js
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  });