Extension to return old Twitter layout from 2015.
1let user = {};
2let cursor;
3let linkColors = {};
4let listId = location.pathname.split('/')[3];
5let subpage;
6// Util
7
8function updateSubpage() {
9 Array.from(document.getElementsByClassName('list-switch')).forEach(el => el.classList.remove('list-switch-active'));
10 document.getElementById('list-members-container').hidden = true;
11 document.getElementById('list-tweets-container').hidden = true;
12 document.getElementById('list-followers-container').hidden = true;
13 end = false;
14 cursor = undefined;
15
16 if(location.href.endsWith('/members')) {
17 subpage = 'members';
18 document.getElementById('list-members-container').hidden = false;
19 document.getElementById('list-members').innerHTML = '';
20 document.getElementById('ns-members').classList.add('list-switch-active');
21 } else if(location.href.endsWith('/followers')) {
22 subpage = 'followers';
23 document.getElementById('list-followers-container').hidden = false;
24 document.getElementById('list-followers').innerHTML = '';
25 document.getElementById('ns-followers').classList.add('list-switch-active');
26 } else {
27 subpage = 'tweets';
28 document.getElementById('list-tweets-container').hidden = false;
29 document.getElementById('list-tweets').innerHTML = '';
30 document.getElementById('ns-tweets').classList.add('list-switch-active');
31 }
32}
33function updateUserData() {
34 API.account.verifyCredentials().then(u => {
35 user = u;
36 userDataFunction(u);
37 renderUserData();
38 }).catch(e => {
39 if (e === "Not logged in") {
40 window.location.href = "https://twitter.com/i/flow/login?newtwitter=true";
41 }
42 console.error(e);
43 });
44}
45// Render
46function renderUserData() {
47 document.getElementById('wtf-viewall').href = `https://twitter.com/i/connect_people?newtwitter=true&user_id=${user.id_str}`;
48}
49
50function renderListData(data) {
51 if(data.custom_banner_media) {
52 document.getElementById('list-banner').src = data.custom_banner_media.media_info.original_img_url;
53 } else {
54 document.getElementById('list-banner').src = data.default_banner_media.media_info.original_img_url;
55 }
56
57 document.getElementsByTagName('title')[0].innerText = `${data.name} List - OldTwitter`;
58 document.getElementById('list-name').innerText = data.name;
59 document.getElementById('list-name').classList.toggle('user-protected', data.mode === 'Private');
60 document.getElementById('list-description').innerText = data.description;
61 document.getElementById('list-members-count').innerText = data.member_count;
62 document.getElementById('list-followers-count').innerText = data.subscriber_count;
63 if(data.user_results && data.user_results.result) {
64 document.getElementById('list-user').href = `https://twitter.com/${data.user_results.result.legacy.screen_name}/lists`;
65 document.getElementById('list-avatar').src = `${(data.user_results.result.legacy.default_profile_image && vars.useOldDefaultProfileImage) ? chrome.runtime.getURL(`images/default_profile_images/default_profile_${Number(data.user_results.result.legacy.id_str) % 7}_normal.png`): data.user_results.result.legacy.profile_image_url_https}`.replace('_normal', '_bigger');
66 let actions = document.getElementById('list-actions');
67 actions.innerHTML = ``;
68 if(data.user_results.result.rest_id === user.id_str) {
69 actions.innerHTML = `
70 <button class="nice-button" id="list-btn-edit">${LOC.edit.message}</button>
71 <button class="nice-button" id="list-btn-delete">${LOC.delete.message}</button>
72 `;
73 document.getElementById('list-btn-edit').addEventListener('click', () => {
74 let modal = createModal(`
75 <div id="list-editor">
76 <h1 class="cool-header">${LOC.edit_list.message}</h1><br>
77 <span id="list-editor-error" style="color:red"></span><br>
78 ${LOC.name.message}:<br><input maxlength="25" type="text" id="list-name-input" value="${escapeHTML(data.name)}"><br><br>
79 ${LOC.description.message}:<br><textarea maxlength="100" type="text" id="list-description-input">${escapeHTML(data.description)}</textarea><br>
80 <br>
81 ${LOC.is_private.message}: <input type="checkbox" style="width: 15px;" id="list-private-input" ${data.mode === 'Private' ? 'checked' : ''}><br>
82 <br>
83 <button class="nice-button" id="list-btn-save">${LOC.save.message}</button>
84 <button class="nice-button" id="list-btn-members">${LOC.edit_members.message}</button>
85 </div>
86 <div id="list-editor-members" hidden>
87 <h1 class="cool-header">${LOC.edit_members.message}</h1>
88 <span id='list-editor-members-back'>${LOC.back.message}</span>
89 <br>
90 <div id="list-editor-members-container"></div>
91 <div class="box" style="border-bottom:none"></div>
92 <div id="list-editor-members-more" class="center-text" style="padding-left: 90px;">${LOC.load_more.message}</div>
93 </div>
94 `, 'list-editor-modal');
95 document.getElementById('list-btn-save').addEventListener('click', async () => {
96 document.getElementById('list-editor-error').innerText = '';
97 let name = document.getElementById('list-name-input').value;
98 let description = document.getElementById('list-description-input').value;
99 let isPrivate = document.getElementById('list-private-input').checked;
100 try {
101 await API.list.update(data.id_str, name, description, isPrivate);
102 document.getElementById('list-name').classList.toggle('user-protected', isPrivate);
103 } catch(e) {
104 return document.getElementById('list-editor-error').innerText = e && e.message ? e.message : e;
105 }
106 modal.remove();
107 renderListData(await API.list.get(data.id_str));
108 });
109 let membersCursor;
110 let membersContainer = document.getElementById('list-editor-members-container');
111 async function getMembers() {
112 let listMembers = await API.list.getMembers(data.id_str, membersCursor);
113 membersCursor = listMembers.cursor;
114 listMembers = listMembers.list;
115 if(!cursor || listMembers.length === 0) document.getElementById('list-editor-members-more').hidden = true;
116 for(let i in listMembers) {
117 let t = listMembers[i];
118 let followingElement = document.createElement('div');
119 followingElement.classList.add('following-item');
120 followingElement.innerHTML = `
121 <div style="height:48px">
122 <a href="https://twitter.com/${t.screen_name}" class="following-item-link">
123 <img src="${(t.default_profile_image && vars.useOldDefaultProfileImage) ? chrome.runtime.getURL(`images/default_profile_images/default_profile_${Number(t.id_str) % 7}_normal.png`): t.profile_image_url_https}" alt="${t.screen_name}" class="following-item-avatar tweet-avatar" width="48" height="48">
124 <div class="following-item-text">
125 <span class="tweet-header-name following-item-name">${escapeHTML(t.name)}</span><br>
126 <span class="tweet-header-handle">@${t.screen_name}</span>
127 ${t.followed_by ? `<span class="follows-you-label">${LOC.follows_you.message}</span>` : ''}
128 </div>
129 </a>
130 </div>
131 <div>
132 <button class="following-item-btn nice-button">${LOC.remove.message}</button>
133 </div>`;
134
135 let removeButton = followingElement.querySelector('.following-item-btn');
136 removeButton.addEventListener('click', async () => {
137 await API.list.removeMember(listId, t.id_str);
138 document.getElementById('list-members-count').innerText = parseInt(document.getElementById('list-members-count').innerText) - 1;
139 followingElement.remove();
140 });
141
142 membersContainer.appendChild(followingElement);
143 if(vars.enableTwemoji) twemoji.parse(followingElement);
144 }
145 }
146 document.getElementById('list-btn-members').addEventListener('click', async () => {
147 document.getElementById('list-editor').hidden = true;
148 document.getElementById('list-editor-members').hidden = false;
149 getMembers();
150 });
151 document.getElementById('list-editor-members-more').addEventListener('click', getMembers);
152 document.getElementById('list-editor-members-back').addEventListener('click', () => {
153 document.getElementById('list-editor').hidden = false;
154 document.getElementById('list-editor-members').hidden = true;
155 });
156 });
157 document.getElementById('list-btn-delete').addEventListener('click', async () => {
158 let modal = createModal(`
159 <h1 class="cool-header">${LOC.delete_list.message}</h1><br>
160 <span>${LOC.delete_list_sure.message}</span>
161 <br><br>
162 <button class="nice-button" id="list-btn-delete-confirm">${LOC.delete.message}</button>
163 `, 'list-editor-modal');
164 document.getElementById('list-btn-delete-confirm').addEventListener('click', async () => {
165 await API.list.delete(data.id_str);
166 modal.remove();
167 window.location.href = `https://twitter.com/${user.screen_name}/lists`;
168 });
169 });
170 } else {
171 actions.innerHTML = `<button class="nice-button" id="list-btn-subscribe">${data.following ? LOC.unsubscribe.message : LOC.subscribe.message}</button>`;
172 document.getElementById('list-btn-subscribe').addEventListener('click', async () => {
173 if(data.following) {
174 await API.list.unsubscribe(data.id_str);
175 document.getElementById('list-followers-count').innerText = +document.getElementById('list-followers-count').innerText - 1;
176 data.following = false;
177 document.getElementById('list-btn-subscribe').innerText = LOC.subscribe.message;
178 } else {
179 await API.list.subscribe(data.id_str);
180 document.getElementById('list-followers-count').innerText = +document.getElementById('list-followers-count').innerText + 1;
181 data.following = true;
182 document.getElementById('list-btn-subscribe').innerText = LOC.unsubscribe.message;
183 }
184 });
185 }
186 }
187}
188async function renderListTweets(c) {
189 let [listInfo, listTweets] = await Promise.allSettled([
190 API.list.get(listId),
191 API.list.getTweets(listId, c)
192 ]).catch(e => {
193 console.error(e);
194 });
195 if(listTweets.reason) {
196 console.error(listTweets.reason);
197 document.getElementById('loading-box').hidden = false;
198 document.getElementById('loading-box-error').innerHTML = `${LOC.list_not_found.message}<br><a href="https://twitter.com/home">${LOC.go_homepage.message}</a>`;
199 return false;
200 }
201 listInfo = listInfo.value;
202 listTweets = listTweets.value;
203 cursor = listTweets.cursor;
204 listTweets = listTweets.list;
205 if(!cursor || listTweets.length === 0) end = true;
206 renderListData(listInfo);
207 let container = document.getElementById('list-tweets');
208 for(let i in listTweets) {
209 let t = listTweets[i];
210 if(t.retweeted_status) {
211 await appendTweet(t.retweeted_status, container, {
212 top: {
213 text: `<a href="https://twitter.com/${t.user.screen_name}">${escapeHTML(t.user.name)}</a> ${LOC.retweeted.message}`,
214 icon: "\uf006",
215 color: "#77b255",
216 class: 'retweet-label'
217 },
218 translate: vars.autotranslateProfiles.includes(t.user.id_str)
219 });
220 } else {
221 await appendTweet(t, container, {
222 bigFont: typeof t.full_text === 'string' && t.full_text.length < 75,
223 translate: vars.autotranslateProfiles.includes(t.user.id_str)
224 });
225 }
226 }
227 return true;
228}
229async function renderListMembers(c) {
230 let [listInfo, listMembers] = await Promise.allSettled([
231 API.list.get(listId),
232 API.list.getMembers(listId, c)
233 ]).catch(e => {
234 console.error(e);
235 });
236 if(listMembers.reason) {
237 console.error(listTweets.reason);
238 document.getElementById('loading-box').hidden = false;
239 document.getElementById('loading-box-error').innerHTML = `${LOC.list_not_found.message}<br><a href="https://twitter.com/home">${LOC.go_homepage.message}</a>`;
240 return false;
241 }
242 listInfo = listInfo.value;
243 listMembers = listMembers.value;
244 cursor = listMembers.cursor;
245 listMembers = listMembers.list;
246 if(!cursor || listMembers.length === 0) end = true;
247 renderListData(listInfo);
248 let container = document.getElementById('list-members');
249 for(let i in listMembers) {
250 let t = listMembers[i];
251 appendUser(t, container);
252 }
253 return true;
254}
255async function renderListFollowers(c) {
256 let [listInfo, listFollowers] = await Promise.allSettled([
257 API.list.get(listId),
258 API.list.getFollowers(listId, c)
259 ]).catch(e => {
260 console.error(e);
261 });
262 if(listFollowers.reason) {
263 console.error(listTweets.reason);
264 document.getElementById('loading-box').hidden = false;
265 document.getElementById('loading-box-error').innerHTML = `${LOC.list_not_found.message}<br><a href="https://twitter.com/home">${LOC.go_homepage.message}</a>`;
266 return false;
267 }
268 listInfo = listInfo.value;
269 listFollowers = listFollowers.value;
270 cursor = listFollowers.cursor;
271 listFollowers = listFollowers.list;
272 if(!cursor || listFollowers.length === 0) end = true;
273 renderListData(listInfo);
274 let container = document.getElementById('list-followers');
275 for(let i in listFollowers) {
276 let t = listFollowers[i];
277 let followingElement = document.createElement('div');
278 followingElement.classList.add('user-item');
279 followingElement.innerHTML = `
280 <div style="height:48px">
281 <a href="https://twitter.com/${t.screen_name}" class="user-item-link">
282 <img src="${(t.default_profile_image && vars.useOldDefaultProfileImage) ? chrome.runtime.getURL(`images/default_profile_images/default_profile_${Number(t.id_str) % 7}_normal.png`): t.profile_image_url_https}" alt="${t.screen_name}" class="user-item-avatar tweet-avatar" width="48" height="48">
283 <div class="user-item-text">
284 <span class="tweet-header-name user-item-name">${escapeHTML(t.name)}</span><br>
285 <span class="tweet-header-handle">@${t.screen_name}</span>
286 </div>
287 </a>
288 </div>
289 <div>
290 <button class="user-item-btn nice-button ${t.following ? 'following' : 'follow'}">${t.following ? LOC.following.message: LOC.follow.message}</button>
291 </div>`;
292
293 let followButton = followingElement.querySelector('.user-item-btn');
294 followButton.addEventListener('click', async () => {
295 if (followButton.classList.contains('following')) {
296 await API.user.unfollow(t.screen_name);
297 followButton.classList.remove('following');
298 followButton.classList.add('follow');
299 followButton.innerText = LOC.follow.message;
300 } else {
301 await API.user.follow(t.screen_name);
302 followButton.classList.remove('follow');
303 followButton.classList.add('following');
304 followButton.innerText = LOC.following.message;
305 }
306 });
307
308 container.appendChild(followingElement);
309 }
310 return true;
311}
312
313async function renderList() {
314 if(subpage === 'tweets') {
315 if(!await renderListTweets(cursor)) return;
316 } else if(subpage === 'members') {
317 if(!await renderListMembers(cursor)) return;
318 } else if(subpage === 'followers') {
319 if(!await renderListFollowers(cursor)) return;
320 }
321 document.getElementById('loading-box').hidden = true;
322 return true;
323}
324
325let lastTweetDate = 0;
326let activeTweet;
327let loadingNewTweets = false;
328let end = false;
329
330setTimeout(async () => {
331 if(!vars) {
332 await loadVars();
333 }
334 // weird bug
335 if(!document.getElementById('wtf-refresh')) {
336 return setTimeout(() => location.reload(), 500);
337 }
338 try {
339 document.getElementById('wtf-refresh').addEventListener('click', async () => {
340 renderDiscovery(false);
341 });
342 } catch(e) {
343 setTimeout(() => location.reload(), 500);
344 console.error(e);
345 return;
346 }
347 window.addEventListener("popstate", async () => {
348 cursor = undefined;
349 updateSubpage();
350 renderList();
351 });
352 document.addEventListener('scroll', async () => {
353 // find active tweet by scroll amount
354 if(Date.now() - lastTweetDate > 50) {
355 lastTweetDate = Date.now();
356 let tweets = Array.from(document.getElementsByClassName('tweet'));
357
358 let scrollPoint = scrollY + innerHeight/2;
359 let newActiveTweet = tweets.find(t => scrollPoint > t.offsetTop && scrollPoint < t.offsetTop + t.offsetHeight);
360 if(!activeTweet || (newActiveTweet && !activeTweet.className.startsWith(newActiveTweet.className))) {
361 if(activeTweet) {
362 activeTweet.classList.remove('tweet-active');
363 }
364 if(newActiveTweet) newActiveTweet.classList.add('tweet-active');
365 if(vars.autoplayVideos && !document.getElementsByClassName('modal')[0]) {
366 if(activeTweet) {
367 let video = activeTweet.querySelector('.tweet-media > video[controls]');
368 if(video) {
369 video.pause();
370 }
371 }
372 if(newActiveTweet) {
373 let newVideo = newActiveTweet.querySelector('.tweet-media > video[controls]');
374 let newVideoOverlay = newActiveTweet.querySelector('.tweet-media > .tweet-media-video-overlay');
375 if(newVideo && !newVideo.ended) {
376 newVideo.play();
377 } else if(newVideoOverlay && !newVideoOverlay.style.display) {
378 newVideoOverlay.click();
379 }
380 }
381 }
382 activeTweet = newActiveTweet;
383 }
384 }
385 // loading new tweets
386 if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 500 && !end) {
387 if (loadingNewTweets) return;
388 loadingNewTweets = true;
389 await renderList();
390 setTimeout(() => {
391 loadingNewTweets = false;
392 }, 250);
393 }
394 });
395
396 document.addEventListener('clearActiveTweet', () => {
397 if(activeTweet) {
398 activeTweet.classList.remove('tweet-active');
399 }
400 activeTweet = undefined;
401 });
402 document.addEventListener('findActiveTweet', () => {
403 let tweets = Array.from(document.getElementsByClassName('tweet'));
404 if(activeTweet) {
405 activeTweet.classList.remove('tweet-active');
406 }
407 let scrollPoint = scrollY + innerHeight/2;
408 activeTweet = tweets.find(t => scrollPoint > t.offsetTop && scrollPoint < t.offsetTop + t.offsetHeight);
409 if(activeTweet) {
410 activeTweet.classList.add('tweet-active');
411 }
412 });
413
414 document.getElementById('list-members-div').addEventListener('click', () => {
415 document.getElementById('ns-members').click();
416 });
417 document.getElementById('list-followers-div').addEventListener('click', () => {
418 document.getElementById('ns-followers').click();
419 });
420
421 let listSwitches = Array.from(document.getElementsByClassName('list-switch'));
422 listSwitches.forEach(s => {
423 s.addEventListener('click', async () => {
424 let id = s.id.split('-')[1];
425 switch(id) {
426 case 'tweets': history.pushState({}, null, `/i/lists/${listId}`); break;
427 case 'members': history.pushState({}, null, `/i/lists/${listId}/members`); break;
428 case 'followers': history.pushState({}, null, `/i/lists/${listId}/followers`); break;
429 }
430 // document.getElementById('loading-box').hidden = false;
431 updateSubpage();
432 cursor = undefined;
433 renderList();
434 });
435 });
436 // Update dates every minute
437 setInterval(() => {
438 let tweetDates = Array.from(document.getElementsByClassName('tweet-time'));
439 let tweetQuoteDates = Array.from(document.getElementsByClassName('tweet-time-quote'));
440 let all = [...tweetDates, ...tweetQuoteDates];
441 all.forEach(date => {
442 date.innerText = timeElapsed(+date.dataset.timestamp);
443 });
444 }, 60000);
445
446 // Run
447 updateSubpage();
448 updateUserData();
449 renderDiscovery();
450 renderList();
451 setInterval(updateUserData, 60000 * 3);
452 setInterval(() => renderDiscovery(false), 60000 * 5);
453}, 50);