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 }