From 83c90b54cb0308815b44fd4ca29f5f21353f70f8 Mon Sep 17 00:00:00 2001 From: Daylin Cooper Date: Sun, 3 Dec 2023 18:02:59 -0700 Subject: [PATCH 1/2] Improved tweet click event handling. There were some issues with tweet click event handling previously, so I fixed them. For user experience, this effectively means that: - Clicking on the text of the tweet (or other "deadzones") will now properly open that tweet. I added two helper functions for determining relationship to a parent. Additionally, I added two extra classes: - tweet-button, which denotes a button behaviour for clickable elements that should not open the tweet. This includes all of the buttons at the bottom of the tweet, as well as the translate button. - tweet-edit-section, for the reply and quote editors, which should also not be interactable. --- scripts/helpers.js | 75 +++++++++++++++++++++++++++--------------- scripts/tweetviewer.js | 23 ++++++------- 2 files changed, 60 insertions(+), 38 deletions(-) diff --git a/scripts/helpers.js b/scripts/helpers.js index 38444c58..5ee6057c 100644 --- a/scripts/helpers.js +++ b/scripts/helpers.js @@ -14,6 +14,34 @@ function arrayBufferToBase64(buffer) { function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } +function isChildOfClass(element, className) { + var cur = element; + while (true) { + if (cur.className && cur.classList.contains(className)) { + return true; + } + if (cur.parentElement != null) { + cur = cur.parentElement; + } else { + break; + } + } + return false; +} +function isChildOfTag(element, tagName) { + var cur = element; + while (true) { + if (cur.tagName && cur.tagName.toUpperCase() == tagName.toUpperCase()) { + return true; + } + if (cur.parentElement != null) { + cur = cur.parentElement; + } else { + break; + } + } + return false; +} function createModal(html, className, onclose, canclose) { let modal = document.createElement('div'); modal.classList.add('modal'); @@ -1615,7 +1643,7 @@ async function appendTweet(t, timelineContainer, options = {}) { if(!options.mainTweet && typeof mainTweetLikers !== 'undefined' && !location.pathname.includes("retweets/with_comments") && !document.querySelector('.modal')) { tweet.addEventListener('click', async e => { - if(e.target.className && (e.target.classList.contains('tweet') || e.target.classList.contains('tweet-body') || e.target.classList.contains('tweet-reply-to') || e.target.className === 'tweet-interact' || e.target.className === 'tweet-media')) { + if (!isChildOfClass(e.target, "tweet-button") && !isChildOfClass(e.target, "tweet-edit-section") && !isChildOfClass(e.target, "dropdown-menu") && !isChildOfClass(e.target, "tweet-media-element") && !isChildOfTag(e.target, "A") && !isChildOfTag(e.target, "BUTTON")) { document.getElementById('loading-box').hidden = false; savePageData(); history.pushState({}, null, `https://twitter.com/${t.user.screen_name}/status/${t.id_str}`); @@ -1641,18 +1669,10 @@ async function appendTweet(t, timelineContainer, options = {}) { currentLocation = location.pathname; } }); - tweet.addEventListener('mousedown', e => { - if(e.button === 1) { - e.preventDefault(); - if(e.target.className && (e.target.classList.contains('tweet') || e.target.classList.contains('tweet-body') || e.target.classList.contains('tweet-reply-to') || e.target.className === 'tweet-interact' || e.target.className === 'tweet-media')) { - openInNewTab(`https://twitter.com/${t.user.screen_name}/status/${t.id_str}`); - } - } - }); } else { if(!options.mainTweet) { tweet.addEventListener('click', e => { - if(e.target.className && (e.target.classList.contains('tweet') || e.target.classList.contains('tweet-body') || e.target.classList.contains('tweet-reply-to') || e.target.className === 'tweet-interact' || e.target.className === 'tweet-media')) { + if(!isChildOfClass(e.target, "tweet-button") && !isChildOfClass(e.target, "tweet-edit-section") && !isChildOfClass(e.target, "dropdown-menu") && !isChildOfClass(e.target, "tweet-media-element") && !isChildOfTag(e.target, "A") && !isChildOfTag(e.target, "BUTTON")) { let tweetData = t; if(tweetData.retweeted_status) tweetData = tweetData.retweeted_status; tweet.classList.add('tweet-preload'); @@ -1663,16 +1683,17 @@ async function appendTweet(t, timelineContainer, options = {}) { new TweetViewer(user, tweetData); } }); - tweet.addEventListener('mousedown', e => { - if(e.button === 1) { - e.preventDefault(); - if(e.target.className && (e.target.classList.contains('tweet') || e.target.classList.contains('tweet-body') || e.target.classList.contains('tweet-reply-to') || e.target.className === 'tweet-interact')) { - openInNewTab(`https://twitter.com/${t.user.screen_name}/status/${t.id_str}`); - } - } - }); } } + tweet.addEventListener('mousedown', e => { + if(e.button === 1) { + // tweet-media-element is clickable, since it should open the tweet in a new tab. + if(!isChildOfClass(e.target, "tweet-button") && !isChildOfClass(e.target, "tweet-edit-section") && !isChildOfClass(e.target, "dropdown-menu") && !isChildOfTag(e.target, "A") && !isChildOfTag(e.target, "BUTTON")) { + e.preventDefault(); + openInNewTab(`https://twitter.com/${t.user.screen_name}/status/${t.id_str}`); + } + } + }); tweet.tabIndex = -1; tweet.className = `tweet ${options.mainTweet ? 'tweet-main' : location.pathname.includes('/status/') ? 'tweet-replying' : ''}`.trim(); tweet.dataset.tweetId = t.id_str; @@ -1847,7 +1868,7 @@ async function appendTweet(t, timelineContainer, options = {}) { ${!isMatchingLanguage && options.mainTweet ? /*html*/`
- ${LOC.view_translation.message} + ${LOC.view_translation.message} ` : ``} ${t.extended_entities && t.extended_entities.media ? /*html*/`
@@ -1893,7 +1914,7 @@ async function appendTweet(t, timelineContainer, options = {}) {
` : ''} ${!isQuoteMatchingLanguage ? /*html*/` - ${LOC.view_translation.message} + ${LOC.view_translation.message} ` : ``} ` : ``} @@ -1933,8 +1954,8 @@ async function appendTweet(t, timelineContainer, options = {}) { ` : ''}
${new Date(t.created_at).toLocaleTimeString(undefined, { hour: 'numeric', minute: 'numeric' }).toLowerCase()} - ${new Date(t.created_at).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' })}  ・ ${t.source ? t.source.split('>')[1].split('<')[0] : 'Unknown'}
- ${options.mainTweet ? '' : formatLargeNumber(t.reply_count).replace(/\s/g, ',')} - ${options.mainTweet ? '' : formatLargeNumber(t.retweet_count).replace(/\s/g, ',')} + ${options.mainTweet ? '' : formatLargeNumber(t.reply_count).replace(/\s/g, ',')} + ${options.mainTweet ? '' : formatLargeNumber(t.retweet_count).replace(/\s/g, ',')} - ${options.mainTweet ? '' : formatLargeNumber(t.favorite_count).replace(/\s/g, ',')} + ${options.mainTweet ? '' : formatLargeNumber(t.favorite_count).replace(/\s/g, ',')} ${(vars.showBookmarkCount || options.mainTweet) && typeof t.bookmark_count !== 'undefined' ? - /*html*/`${formatLargeNumber(t.bookmark_count).replace(/\s/g, ',')}` : + /*html*/`${formatLargeNumber(t.bookmark_count).replace(/\s/g, ',')}` : ''} ${vars.seeTweetViews && t.ext && t.ext.views && t.ext.views.r && t.ext.views.r.ok && t.ext.views.r.ok.count ? /*html*/`${formatLargeNumber(t.ext.views.r.ok.count).replace(/\s/g, ',')}` : ''} - + -