Extension to return old Twitter layout from 2015.
at master 422 lines 21 kB view raw
1let user = {}; 2let subpage; 3let loadingMore = false; 4 5// Util 6 7function updateSubpage() { 8 lastData = undefined; 9 if(location.pathname.includes('notifications/mentions')) { 10 subpage = 'mentions'; 11 document.getElementById('ns-m').classList.add('notification-switch-active'); 12 document.getElementById('ns-n').classList.remove('notification-switch-active'); 13 } else { 14 subpage = 'notifications'; 15 document.getElementById('ns-n').classList.add('notification-switch-active'); 16 document.getElementById('ns-m').classList.remove('notification-switch-active'); 17 }; 18} 19 20function updateUserData() { 21 API.account.verifyCredentials().then(u => { 22 user = u; 23 userDataFunction(u); 24 renderUserData(); 25 }).catch(e => { 26 if (e === "Not logged in") { 27 window.location.href = "https://twitter.com/i/flow/login?newtwitter=true"; 28 } 29 console.error(e); 30 }); 31} 32// Render 33function renderUserData() { 34 document.getElementById('wtf-viewall').href = `https://twitter.com/i/connect_people?newtwitter=true&user_id=${user.id_str}`; 35} 36 37let lastFirstCursor = undefined; 38let lastCursor = undefined; 39let aRegex = /<a[^>]*>([\s\S]*?)<\/a>/g; 40let firstRender = true; 41async function renderNotifications(data, append = false) { 42 let notificationsContainer = document.getElementById('notifications-div'); 43 let entries = data.timeline.instructions.find(i => i.addEntries).addEntries.entries; 44 let cursor = entries[0].content.operation.cursor.value; 45 if(lastFirstCursor === cursor) return; 46 lastFirstCursor = cursor; 47 if(!append) notificationsContainer.innerHTML = ''; 48 let unreadBefore = +data.timeline.instructions.find(i => i.markEntriesUnreadGreaterThanSortIndex).markEntriesUnreadGreaterThanSortIndex.sortIndex; 49 let unreadNotifications = 0; 50 let errorMsg = undefined; 51 for(let i in entries) { 52 if(i === 0) continue; 53 let e = entries[i]; 54 e = e.content.item; 55 if(!e) continue; 56 try { 57 if(e.content.notification) { 58 let n = data.globalObjects.notifications[e.content.notification.id]; 59 if(!n) continue; 60 if(e.feedbackInfo) { 61 n.feedback = data.timeline.responseObjects.feedbackActions[e.feedbackInfo.feedbackKeys[0]]; 62 if(n.feedback) n.feedback.metadata = e.feedbackInfo.feedbackMetadata; 63 } 64 let notificationDiv = document.createElement('div'); 65 notificationDiv.className = 'notification'; 66 if(+entries[i].sortIndex > unreadBefore) { 67 notificationDiv.classList.add('notification-unread'); 68 unreadNotifications++; 69 } 70 let replyTweet = n.template.aggregateUserActionsV1.targetObjects[0] ? data.globalObjects.tweets[n.template.aggregateUserActionsV1.targetObjects[0].tweet.id] : { full_text: '' }; 71 if(replyTweet && replyTweet.user_id_str) {; 72 if(replyTweet.quoted_status_id_str) { 73 replyTweet.quoted_status = data.globalObjects.tweets[replyTweet.quoted_status_id_str]; 74 if(replyTweet.quoted_status) { 75 replyTweet.quoted_status.user = data.globalObjects.users[replyTweet.quoted_status.user_id_str]; 76 replyTweet.quoted_status.user.id_str = replyTweet.quoted_status.user_id_str; 77 } 78 } 79 if(replyTweet.retweeted_status_id_str) { 80 replyTweet.retweeted_status = data.globalObjects.tweets[replyTweet.retweeted_status_id_str]; 81 if(replyTweet.retweeted_status) { 82 replyTweet.retweeted_status.user = data.globalObjects.users[replyTweet.retweeted_status.user_id_str]; 83 replyTweet.retweeted_status.user.id_str = replyTweet.retweeted_status.user_id_str; 84 } 85 } 86 let replyUser = replyTweet ? data.globalObjects.users[replyTweet.user_id_str] : undefined; 87 replyUser.id_str = replyTweet.user_id_str; 88 replyTweet.user = replyUser; 89 } 90 let E = e; 91 notificationDiv.addEventListener('click', e => { 92 if(e.target.closest('.notification') && e.target.tagName !== 'IMG' && e.target.tagName !== 'A' && e.target.className !== 'notification-feedback') { 93 if(n.icon.id === "bell_icon") { 94 location.href = `https://twitter.com/i/timeline?page=device_follow&nid=${n.id}`; 95 } else if(n.icon.id === "heart_icon") { 96 if(notificationHeader.includes('Tweets')) { 97 location.href = `https://twitter.com/i/timeline?page=likes&nid=${n.id}`; 98 } else { 99 new TweetViewer(user, replyTweet.retweeted_status ? replyTweet.retweeted_status : replyTweet); 100 } 101 } else if(n.icon.id === "list_icon") { 102 location.href = E.content.notification.url.url; 103 } else if(replyTweet && replyTweet.user) { 104 new TweetViewer(user, replyTweet.retweeted_status ? replyTweet.retweeted_status : replyTweet); 105 } 106 } 107 }); 108 notificationDiv.addEventListener('mousedown', e => { 109 if(e.target.tagName === 'A' || e.target.className === 'notification-avatar-img') { 110 let url = new URL(e.target.href); 111 if(isProfilePath(url.pathname)) { 112 return; 113 } 114 }; 115 if(e.button === 1) { 116 e.preventDefault(); 117 if(n.icon.id === "bell_icon") { 118 openInNewTab(`https://twitter.com/i/timeline?page=device_follow&nid=${n.id}`); 119 } else if(n.icon.id === "heart_icon") { 120 openInNewTab(`https://twitter.com/i/timeline?page=likes&nid=${n.id}`); 121 } else if(n.icon.id === "list_icon") { 122 openInNewTab(E.content.notification.url.url); 123 } else if(e.target.closest('.notification') && e.target.tagName !== 'IMG') { 124 if(replyTweet.retweeted_status) { 125 openInNewTab(`https://twitter.com/${replyTweet.retweeted_status.user.screen_name}/status/${replyTweet.retweeted_status.id_str}`); 126 } else { 127 openInNewTab(`https://twitter.com/${replyTweet.user.screen_name}/status/${replyTweet.id_str}`); 128 } 129 } 130 } 131 }); 132 let notificationHeader = n.message.text; 133 if (n.message.entities) { 134 let additionalLength = 0; 135 let matches = 0; 136 n.message.entities.forEach(e => { 137 if(!e.ref || !e.ref.user) return; 138 let user = data.globalObjects.users[e.ref.user.id]; 139 notificationHeader = Array.from(notificationHeader); 140 notificationHeader = arrayInsert(notificationHeader, e.toIndex+additionalLength, '</a>'); 141 notificationHeader = arrayInsert(notificationHeader, e.fromIndex+additionalLength, `<a href="/dimdenEFF">`); 142 notificationHeader = notificationHeader.join(''); 143 additionalLength += `<a href="/dimdenEFF"></a>`.length; 144 let mi = 0; 145 let newText = notificationHeader.replace(aRegex, (_, m) => { 146 if(mi++ !== matches) return _; 147 return `<a href="/${user.screen_name}"${user.verified ? 'class="user-verified"' : ''}>${escapeHTML(m)}</a>`; 148 }); 149 additionalLength += newText.length - notificationHeader.length; 150 notificationHeader = newText; 151 matches++; 152 }); 153 }; 154 let users = n.template.aggregateUserActionsV1.fromUsers.map(u => data.globalObjects.users[u.user.id]); 155 156 if(n.icon.id === 'recommendation_icon') { 157 notificationHeader = `<b><a href="https://twitter.com/${users[0].screen_name}">${escapeHTML(notificationHeader)}</a></b>`; 158 } 159 160 let iconClasses = { 161 'heart_icon': 'ni-favorite', 162 'person_icon': 'ni-follow', 163 'retweet_icon': 'ni-retweet', 164 'recommendation_icon': 'ni-recommend', 165 'lightning_bolt_icon': 'ni-bolt', 166 'bird_icon': 'ni-twitter', 167 'security_alert_icon': 'ni-alert', 168 'bell_icon': 'ni-bell', 169 'list_icon': 'ni-list' 170 }; 171 if(!iconClasses[n.icon.id]) { 172 console.log(`Unsupported icon: "${n.icon.id}". Report it to https://github.com/dimdenGD/OldTwitter/issues`); 173 } 174 if(n.icon.id === 'heart_icon' && !vars.heartsNotStars) { 175 notificationHeader = notificationHeader.replace(' liked ', ' favorited '); 176 } 177 notificationDiv.innerHTML = /*html*/` 178 <div class="notification-icon ${iconClasses[n.icon.id]}"></div> 179 <div class="notification-header"> 180 ${notificationHeader} ${n.feedback ? `<span class="notification-feedback">[${n.feedback.prompt}]</span>` : ''} 181 </div> 182 <div class="notification-text">${escapeHTML(replyTweet.full_text.replace(/^(@[\w+]{1,15}\b\s)((@[\w+]{1,15}\b\s)+)/g, '$1'))}</div> 183 <div class="notification-avatars"> 184 ${users.map(u => `<a class="notification-avatar" href="/${u.screen_name}"><img class="notification-avatar-img" src="${`${(u.default_profile_image && vars.useOldDefaultProfileImage) ? chrome.runtime.getURL(`images/default_profile_images/default_profile_${Number(u.id_str) % 7}_normal.png`): u.profile_image_url_https}`.replace("_normal", "_bigger")}" alt="${escapeHTML(u.name)}" width="32" height="32"></a>`).join('')} 185 </div> 186 `; 187 let notifText = notificationDiv.querySelector('.notification-text'); 188 if(replyTweet.entities && replyTweet.entities.urls) { 189 for(let url of replyTweet.entities.urls) { 190 notifText.innerText = notifText.innerText.replace(new RegExp(url.url, "g"), url.display_url); 191 } 192 } 193 notificationDiv.dataset.notificationId = n.id; 194 if(n.feedback) { 195 let feedbackBtn = notificationDiv.querySelector('.notification-feedback'); 196 feedbackBtn.addEventListener('click', () => { 197 fetch('/i/api/2/notifications/feedback.json?' + n.feedback.feedbackUrl.split('?').slice(1).join('?'), { 198 headers: { 199 "authorization": OLDTWITTER_CONFIG.public_token, 200 "x-csrf-token": OLDTWITTER_CONFIG.csrf, 201 "content-type": "application/x-www-form-urlencoded", 202 "x-twitter-auth-type": "OAuth2Session", 203 "x-twitter-client-language": LANGUAGE || navigator.language, 204 "x-twitter-active-user": "yes" 205 }, 206 method: 'post', 207 credentials: 'include', 208 body: `feedback_type=${n.feedback.feedbackType}&feedback_metadata=${n.feedback.metadata}&undo=false` 209 }).then(i => i.text()).then(i => { 210 notificationDiv.remove(); 211 alert(n.feedback.confirmation); 212 }); 213 }); 214 } 215 notificationsContainer.append(notificationDiv); 216 if(vars.enableTwemoji) twemoji.parse(notificationDiv); 217 } else if(e.content.tweet) { 218 let t = data.globalObjects.tweets[e.content.tweet.id]; 219 t.user = data.globalObjects.users[t.user_id_str]; 220 if(t.quoted_status_id_str) { 221 t.quoted_status = data.globalObjects.tweets[t.quoted_status_id_str]; 222 t.quoted_status.user = data.globalObjects.users[t.quoted_status.user_id_str]; 223 } 224 if(!t) continue; 225 let tweet = await appendTweet(t, notificationsContainer, { 226 bigFont: t.full_text.length < 75 227 }); 228 if(+entries[i].sortIndex > unreadBefore) { 229 tweet.classList.add('notification-unread'); 230 unreadNotifications++; 231 } 232 } 233 } catch(e) { 234 errorMsg = e; 235 console.error(e); 236 } 237 } 238 if(errorMsg) { 239 createModal(` 240 <div style="max-width:700px"> 241 <span style="font-size:14px;color:var(--default-text-color)"> 242 <h2 style="margin-top: 0">${LOC.something_went_wrong.message}</h2> 243 ${LOC.notifications_error.message}<br> 244 ${LOC.error_instructions.message.replace('$AT1$', "<a target='_blank' href='https://github.com/dimdenGD/OldTwitter/issues'>").replace(/\$AT2\$/g, '</a>').replace("$AT3$", "<a target='_blank' href='mailto:admin@dimden.dev'>")} 245 </span> 246 <div class="box" style="font-family:monospace;line-break: anywhere;padding:5px;margin-top:5px;background:rgba(255, 0, 0, 0.2)"> 247 ${escapeHTML(errorMsg.stack ? errorMsg.stack : String(errorMsg))} (OldTwitter v${chrome.runtime.getManifest().version}) 248 </div> 249 </div> 250 `) 251 } 252 if(unreadNotifications > 0) { 253 setTimeout(() => { 254 API.notifications.markAsRead(cursor); 255 if(windowFocused) { 256 firstRender = false; 257 document.getElementById('site-icon').href = chrome.runtime.getURL(`images/logo32${vars.useNewIcon ? '_new' : ''}_notification.png`); 258 let newTitle = document.title; 259 if(document.title.startsWith('(')) { 260 newTitle = document.title.split(') ')[1]; 261 } 262 newTitle = `(${unreadNotifications}) ${newTitle}`; 263 if(document.title !== newTitle) { 264 document.title = newTitle; 265 } 266 notificationBus.postMessage({type: 'markAsRead', cursor}); 267 } 268 }, 500); 269 } 270} 271let lastData; 272async function updateNotifications(append = false, quiet = false) { 273 if(!append && !quiet) { 274 document.getElementById('notifs-loading').hidden = false; 275 document.getElementById('notifications-more').hidden = true; 276 } 277 let data; 278 279 try { 280 data = await API.notifications.get(append ? lastCursor : undefined, subpage === 'mentions'); 281 } catch(e) { 282 await sleep(2500); 283 try { 284 data = await API.notifications.get(append ? lastCursor : undefined, subpage === 'mentions'); 285 } catch(e) { 286 document.getElementById('notifs-loading').hidden = true; 287 document.getElementById('notifications-more').hidden = false; 288 document.getElementById('notifications-more').innerText = LOC.load_more.message; 289 loadingMore = false; 290 console.error(e); 291 return; 292 } 293 } 294 if(append || !lastCursor) { 295 let entries = data.timeline.instructions.find(i => i.addEntries).addEntries.entries; 296 lastCursor = entries[entries.length-1].content.operation.cursor.value; 297 } 298 if(!append && lastData) { 299 let lastCursorTop = lastData.timeline.instructions.find(i => i.addEntries).addEntries.entries[0].entryId; 300 let cursorTop = data.timeline.instructions.find(i => i.addEntries).addEntries.entries[0].entryId; 301 if(lastCursorTop === cursorTop) { 302 return; 303 } 304 } 305 lastData = data; 306 await renderNotifications(data, append); 307 document.getElementById('notifs-loading').hidden = true; 308 document.getElementById('notifications-more').hidden = false; 309 document.getElementById('notifications-more').innerText = LOC.load_more.message; 310 loadingMore = false; 311 document.getElementById('loading-box').hidden = true; 312} 313 314let windowFocused = document.hidden; 315 316setTimeout(async () => { 317 if(!vars) { 318 await loadVars(); 319 } 320 321 // weird bug 322 if(!document.getElementById('wtf-refresh')) { 323 return setTimeout(() => location.reload(), 500); 324 } 325 try { 326 document.getElementById('wtf-refresh').addEventListener('click', async () => { 327 renderDiscovery(false); 328 }); 329 } catch(e) { 330 setTimeout(() => location.reload(), 500); 331 console.error(e); 332 return; 333 } 334 document.getElementById('notifs-loading').children[0].src = chrome.runtime.getURL(`images/loading.svg`); 335 336 windowFocused = document.hidden; 337 onVisibilityChange(vis => { 338 windowFocused = vis; 339 if(vis) { 340 notificationBus.postMessage({type: 'markAsRead', cursor: undefined}); 341 document.getElementById('site-icon').href = chrome.runtime.getURL(`images/logo32${vars.useNewIcon ? '_new' : ''}.png`); 342 } 343 }); 344 345 // buttons 346 document.getElementById('notifications-more').addEventListener('click', async () => { 347 if(!lastCursor) return; 348 if(loadingMore) return; 349 350 loadingMore = true; 351 document.getElementById('notifications-more').innerText = LOC.loading.message; 352 updateNotifications(true); 353 }); 354 document.getElementById('ns-m').addEventListener('click', async () => { 355 lastCursor = undefined; 356 history.pushState({}, null, '/notifications/mentions'); 357 document.getElementById('notifs-loading').hidden = false; 358 document.getElementById('notifications-more').hidden = true; 359 document.getElementById('notifications-div').innerHTML = ``; 360 updateSubpage(); 361 updateNotifications(); 362 }); 363 document.getElementById('ns-n').addEventListener('click', async () => { 364 lastCursor = undefined; 365 history.pushState({}, null, '/notifications'); 366 document.getElementById('notifs-loading').hidden = false; 367 document.getElementById('notifications-more').hidden = true; 368 document.getElementById('notifications-div').innerHTML = ``; 369 updateSubpage(); 370 updateNotifications(); 371 }); 372 window.addEventListener("popstate", async () => { 373 lastCursor = undefined; 374 updateSubpage(); 375 updateNotifications(); 376 }); 377 378 let search = new URLSearchParams(location.search); 379 if(search.get('nonavbar') === '1') { 380 document.getElementById('navbar').hidden = true; 381 document.getElementById('navbar-line').hidden = true; 382 document.getElementById('notification-switches').style.top = '5px'; 383 document.getElementById('notifications-div').style.marginTop = '16px'; 384 385 let root = document.querySelector(":root"); 386 let bg = root.style.getPropertyValue('--background-color'); 387 root.style.setProperty('--darker-background-color', bg); 388 } 389 390 // Update dates every minute 391 setInterval(() => { 392 let tweetDates = Array.from(document.getElementsByClassName('tweet-time')); 393 let tweetQuoteDates = Array.from(document.getElementsByClassName('tweet-time-quote')); 394 let all = [...tweetDates, ...tweetQuoteDates]; 395 all.forEach(date => { 396 date.innerText = timeElapsed(+date.dataset.timestamp); 397 }); 398 }, 60000); 399 400 401 // Run 402 updateSubpage(); 403 updateUserData(); 404 renderDiscovery(); 405 renderTrends(); 406 updateNotifications(); 407 setInterval(updateUserData, 60000 * 3); 408 setInterval(() => renderDiscovery(false), 60000 * 5); 409 setInterval(renderTrends, 60000 * 5); 410 setInterval(() => { 411 if(document.scrollingElement.scrollTop > 3000) return; 412 let modal = document.querySelector('.modal'); 413 if(!modal) { 414 if(document.querySelector('.tweet-reply:not([hidden])') || document.querySelector('.tweet-quote:not([hidden])')) { 415 return; 416 } 417 } 418 updateNotifications(false, true); 419 }, 20000); 420 421 document.getElementById('loading-box').hidden = true; 422}, 50);