Extension to return old Twitter layout from 2015.
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);