/ src / utils / timeline-utils.jsx
timeline-utils.jsx
  1  import store from './store';
  2  import { api } from './api';
  3  
  4  export function groupBoosts(values) {
  5    let newValues = [];
  6    let boostStash = [];
  7    let serialBoosts = 0;
  8    for (let i = 0; i < values.length; i++) {
  9      const item = values[i];
 10      if (item.reblog && !item.account?.group) {
 11        boostStash.push(item);
 12        serialBoosts++;
 13      } else {
 14        newValues.push(item);
 15        if (serialBoosts < 3) {
 16          serialBoosts = 0;
 17        }
 18      }
 19    }
 20    // if boostStash is more than quarter of values
 21    // or if there are 3 or more boosts in a row
 22    if (
 23      values.length > 10 &&
 24      (boostStash.length > values.length / 4 || serialBoosts >= 3)
 25    ) {
 26      // if boostStash is more than 3 quarter of values
 27      const boostStashID = boostStash.map((status) => status.id);
 28      if (boostStash.length > (values.length * 3) / 4) {
 29        // insert boost array at the end of specialHome list
 30        newValues = [
 31          ...newValues,
 32          { id: boostStashID, items: boostStash, type: 'boosts' },
 33        ];
 34      } else {
 35        // insert boosts array in the middle of specialHome list
 36        const half = Math.floor(newValues.length / 2);
 37        newValues = [
 38          ...newValues.slice(0, half),
 39          {
 40            id: boostStashID,
 41            items: boostStash,
 42            type: 'boosts',
 43          },
 44          ...newValues.slice(half),
 45        ];
 46      }
 47      return newValues;
 48    } else {
 49      return values;
 50    }
 51  }
 52  
 53  export function applyMutedWords(values) {
 54    const { masto } = api();
 55    (async () => {
 56      try {
 57        const filterResults = await masto.v2.filters.fetch({
 58          resolve: true
 59        });
 60        if (filterResults) {
 61          let newValues = [];
 62          let allFilters = [];
 63          filterResults.forEach((filterList) => {
 64            filterList.keywords.forEach((keyword) => {
 65              allFilters.push(keyword);
 66            });
 67          });
 68          console.log(`users filters: ${filterResults}`);
 69          values.forEach((value) => {
 70            if (value) {
 71              allFilters.forEach((filteredWord) => {
 72                if (value.content?.indexOf(filteredWord) > -1) {
 73                  newValues.push(value);
 74                }
 75              });
 76            }
 77          });
 78          return newValues;
 79        } else {
 80          return values;
 81        }
 82      } catch (e) {
 83        alert('Error: ' + e);
 84        console.error(e);
 85      }
 86    })();
 87  }
 88  
 89  export function dedupeBoosts(items, instance) {
 90    const boostedStatusIDs = store.account.get('boostedStatusIDs') || {};
 91    const filteredItems = items.filter((item) => {
 92      if (!item.reblog) return true;
 93      const statusKey = `${instance}-${item.reblog.id}`;
 94      const boosterID = boostedStatusIDs[statusKey];
 95      if (boosterID && boosterID !== item.id) {
 96        console.warn(
 97          `🚫 Duplicate boost by ${item.account.displayName}`,
 98          item,
 99          item.reblog,
100        );
101        return false;
102      } else {
103        boostedStatusIDs[statusKey] = item.id;
104      }
105      return true;
106    });
107    // Limit to 50
108    const keys = Object.keys(boostedStatusIDs);
109    if (keys.length > 50) {
110      keys.slice(0, keys.length - 50).forEach((key) => {
111        delete boostedStatusIDs[key];
112      });
113    }
114    store.account.set('boostedStatusIDs', boostedStatusIDs);
115    return filteredItems;
116  }
117  
118  export function groupContext(items) {
119    const contexts = [];
120    let contextIndex = 0;
121    items.forEach((item) => {
122      for (let i = 0; i < contexts.length; i++) {
123        if (contexts[i].find((t) => t.id === item.id)) return;
124        if (
125          contexts[i].find((t) => t.id === item.inReplyToId) ||
126          contexts[i].find((t) => t.inReplyToId === item.id)
127        ) {
128          contexts[i].push(item);
129          return;
130        }
131      }
132      const repliedItem = items.find((i) => i.id === item.inReplyToId);
133      if (repliedItem) {
134        contexts[contextIndex++] = [item, repliedItem];
135      }
136    });
137  
138    // Check for cross-item contexts
139    // Merge contexts into one if they have a common item (same id)
140    for (let i = 0; i < contexts.length; i++) {
141      for (let j = i + 1; j < contexts.length; j++) {
142        const commonItem = contexts[i].find((t) => contexts[j].includes(t));
143        if (commonItem) {
144          contexts[i] = [...contexts[i], ...contexts[j]];
145          // Remove duplicate items
146          contexts[i] = contexts[i].filter(
147            (item, index, self) =>
148              self.findIndex((t) => t.id === item.id) === index,
149          );
150          contexts.splice(j, 1);
151          j--;
152        }
153      }
154    }
155  
156    // Sort items by checking inReplyToId
157    contexts.forEach((context) => {
158      context.sort((a, b) => {
159        if (!a.inReplyToId && !b.inReplyToId) {
160          return new Date(a.createdAt) - new Date(b.createdAt);
161        }
162        if (a.inReplyToId === b.id) return 1;
163        if (b.inReplyToId === a.id) return -1;
164        if (!a.inReplyToId) return -1;
165        if (!b.inReplyToId) return 1;
166        return new Date(a.createdAt) - new Date(b.createdAt);
167      });
168    });
169  
170    // Tag items that has different author than first post's author
171    contexts.forEach((context) => {
172      const firstItemAccountID = context[0].account.id;
173      context.forEach((item) => {
174        if (item.account.id !== firstItemAccountID) {
175          item._differentAuthor = true;
176        }
177      });
178    });
179  
180    if (contexts.length) console.log('🧵 Contexts', contexts);
181  
182    const newItems = [];
183    const appliedContextIndices = [];
184    items.forEach((item) => {
185      if (item.reblog) {
186        newItems.push(item);
187        return;
188      }
189      for (let i = 0; i < contexts.length; i++) {
190        if (contexts[i].find((t) => t.id === item.id)) {
191          if (appliedContextIndices.includes(i)) return;
192          const contextItems = contexts[i];
193          contextItems.sort((a, b) => {
194            const aDate = new Date(a.createdAt);
195            const bDate = new Date(b.createdAt);
196            return aDate - bDate;
197          });
198          const firstItemAccountID = contextItems[0].account.id;
199          newItems.push({
200            id: contextItems.map((i) => i.id),
201            items: contextItems,
202            type: contextItems.every((it) => it.account.id === firstItemAccountID)
203              ? 'thread'
204              : 'conversation',
205          });
206          appliedContextIndices.push(i);
207          return;
208        }
209      }
210      newItems.push(item);
211    });
212  
213    return newItems;
214  }