diff --git a/Configuration/UserConfig.cs b/Configuration/UserConfig.cs index d14320eb..1344818d 100644 --- a/Configuration/UserConfig.cs +++ b/Configuration/UserConfig.cs @@ -26,6 +26,7 @@ sealed class UserConfig : BaseConfig<UserConfig>, IAppUserConfiguration { public bool KeepLikeFollowDialogsOpen { get; set; } = true; public bool BestImageQuality { get; set; } = true; public bool EnableAnimatedImages { get; set; } = true; + public bool HideTweetsByNftUsers { get; set; } = false; private bool _enableSmoothScrolling = true; private string _customCefArgs = null; diff --git a/Dialogs/FormSettings.cs b/Dialogs/FormSettings.cs index 368eab3d..a651a263 100644 --- a/Dialogs/FormSettings.cs +++ b/Dialogs/FormSettings.cs @@ -40,7 +40,7 @@ public FormSettings(FormBrowser browser, PluginManager plugins, UpdateChecker up PrepareLoad(); - AddButton("General", () => new TabSettingsGeneral(tweetDeckFunctions.ReloadColumns, updates)); + AddButton("General", () => new TabSettingsGeneral(this.browser.ReloadToTweetDeck, tweetDeckFunctions.ReloadColumns, updates)); AddButton("Notifications", () => new TabSettingsNotifications(this.browser.CreateExampleNotification())); AddButton("Sounds", () => new TabSettingsSounds(() => tweetDeckFunctions.PlaySoundNotification(true))); AddButton("Tray", () => new TabSettingsTray()); diff --git a/Dialogs/Settings/TabSettingsGeneral.Designer.cs b/Dialogs/Settings/TabSettingsGeneral.Designer.cs index 92f64a0b..a6a4f01f 100644 --- a/Dialogs/Settings/TabSettingsGeneral.Designer.cs +++ b/Dialogs/Settings/TabSettingsGeneral.Designer.cs @@ -42,6 +42,8 @@ private void InitializeComponent() { this.checkFocusDmInput = new System.Windows.Forms.CheckBox(); this.checkKeepLikeFollowDialogsOpen = new System.Windows.Forms.CheckBox(); this.checkSmoothScrolling = new System.Windows.Forms.CheckBox(); + this.labelTweetDeckSettings = new System.Windows.Forms.Label(); + this.checkHideTweetsByNftUsers = new System.Windows.Forms.CheckBox(); this.labelBrowserPath = new System.Windows.Forms.Label(); this.comboBoxCustomBrowser = new System.Windows.Forms.ComboBox(); this.labelSearchEngine = new System.Windows.Forms.Label(); @@ -87,11 +89,11 @@ private void InitializeComponent() { // this.checkUpdateNotifications.AutoSize = true; this.checkUpdateNotifications.Font = new System.Drawing.Font("Segoe UI", 9F); - this.checkUpdateNotifications.Location = new System.Drawing.Point(6, 307); + this.checkUpdateNotifications.Location = new System.Drawing.Point(6, 381); this.checkUpdateNotifications.Margin = new System.Windows.Forms.Padding(6, 6, 3, 2); this.checkUpdateNotifications.Name = "checkUpdateNotifications"; this.checkUpdateNotifications.Size = new System.Drawing.Size(182, 19); - this.checkUpdateNotifications.TabIndex = 11; + this.checkUpdateNotifications.TabIndex = 13; this.checkUpdateNotifications.Text = "Check Updates Automatically"; this.checkUpdateNotifications.UseVisualStyleBackColor = true; // @@ -99,12 +101,12 @@ private void InitializeComponent() { // this.btnCheckUpdates.AutoSize = true; this.btnCheckUpdates.Font = new System.Drawing.Font("Segoe UI", 9F); - this.btnCheckUpdates.Location = new System.Drawing.Point(5, 331); + this.btnCheckUpdates.Location = new System.Drawing.Point(5, 405); this.btnCheckUpdates.Margin = new System.Windows.Forms.Padding(5, 3, 3, 3); this.btnCheckUpdates.Name = "btnCheckUpdates"; this.btnCheckUpdates.Padding = new System.Windows.Forms.Padding(2, 0, 2, 0); this.btnCheckUpdates.Size = new System.Drawing.Size(128, 25); - this.btnCheckUpdates.TabIndex = 12; + this.btnCheckUpdates.TabIndex = 14; this.btnCheckUpdates.Text = "Check Updates Now"; this.btnCheckUpdates.UseVisualStyleBackColor = true; // @@ -124,12 +126,12 @@ private void InitializeComponent() { // this.checkBestImageQuality.AutoSize = true; this.checkBestImageQuality.Font = new System.Drawing.Font("Segoe UI", 9F); - this.checkBestImageQuality.Location = new System.Drawing.Point(6, 122); - this.checkBestImageQuality.Margin = new System.Windows.Forms.Padding(6, 3, 3, 2); + this.checkBestImageQuality.Location = new System.Drawing.Point(6, 259); + this.checkBestImageQuality.Margin = new System.Windows.Forms.Padding(6, 6, 3, 2); this.checkBestImageQuality.Name = "checkBestImageQuality"; - this.checkBestImageQuality.Size = new System.Drawing.Size(125, 19); - this.checkBestImageQuality.TabIndex = 5; - this.checkBestImageQuality.Text = "Best Image Quality"; + this.checkBestImageQuality.Size = new System.Drawing.Size(182, 19); + this.checkBestImageQuality.TabIndex = 9; + this.checkBestImageQuality.Text = "Download Best Image Quality"; this.checkBestImageQuality.UseVisualStyleBackColor = true; // // checkOpenSearchInFirstColumn @@ -163,11 +165,11 @@ private void InitializeComponent() { // this.labelZoom.AutoSize = true; this.labelZoom.Font = new System.Drawing.Font("Segoe UI Semibold", 9F, System.Drawing.FontStyle.Bold); - this.labelZoom.Location = new System.Drawing.Point(3, 203); + this.labelZoom.Location = new System.Drawing.Point(3, 155); this.labelZoom.Margin = new System.Windows.Forms.Padding(3, 12, 3, 0); this.labelZoom.Name = "labelZoom"; this.labelZoom.Size = new System.Drawing.Size(39, 15); - this.labelZoom.TabIndex = 8; + this.labelZoom.TabIndex = 6; this.labelZoom.Text = "Zoom"; // // zoomUpdateTimer @@ -179,21 +181,21 @@ private void InitializeComponent() { // this.panelZoom.Controls.Add(this.trackBarZoom); this.panelZoom.Controls.Add(this.labelZoomValue); - this.panelZoom.Location = new System.Drawing.Point(0, 219); + this.panelZoom.Location = new System.Drawing.Point(0, 171); this.panelZoom.Margin = new System.Windows.Forms.Padding(0, 1, 0, 0); this.panelZoom.Name = "panelZoom"; this.panelZoom.Size = new System.Drawing.Size(300, 35); - this.panelZoom.TabIndex = 9; + this.panelZoom.TabIndex = 7; // // checkAnimatedAvatars // this.checkAnimatedAvatars.AutoSize = true; this.checkAnimatedAvatars.Font = new System.Drawing.Font("Segoe UI", 9F); - this.checkAnimatedAvatars.Location = new System.Drawing.Point(6, 146); + this.checkAnimatedAvatars.Location = new System.Drawing.Point(6, 307); this.checkAnimatedAvatars.Margin = new System.Windows.Forms.Padding(6, 3, 3, 2); this.checkAnimatedAvatars.Name = "checkAnimatedAvatars"; this.checkAnimatedAvatars.Size = new System.Drawing.Size(158, 19); - this.checkAnimatedAvatars.TabIndex = 6; + this.checkAnimatedAvatars.TabIndex = 11; this.checkAnimatedAvatars.Text = "Enable Animated Avatars"; this.checkAnimatedAvatars.UseVisualStyleBackColor = true; // @@ -201,11 +203,11 @@ private void InitializeComponent() { // this.labelUpdates.AutoSize = true; this.labelUpdates.Font = new System.Drawing.Font("Segoe UI Semibold", 10.5F, System.Drawing.FontStyle.Bold); - this.labelUpdates.Location = new System.Drawing.Point(0, 281); + this.labelUpdates.Location = new System.Drawing.Point(0, 355); this.labelUpdates.Margin = new System.Windows.Forms.Padding(0, 27, 0, 1); this.labelUpdates.Name = "labelUpdates"; this.labelUpdates.Size = new System.Drawing.Size(69, 19); - this.labelUpdates.TabIndex = 10; + this.labelUpdates.TabIndex = 12; this.labelUpdates.Text = "UPDATES"; // // flowPanelLeft @@ -217,11 +219,13 @@ private void InitializeComponent() { this.flowPanelLeft.Controls.Add(this.checkFocusDmInput); this.flowPanelLeft.Controls.Add(this.checkOpenSearchInFirstColumn); this.flowPanelLeft.Controls.Add(this.checkKeepLikeFollowDialogsOpen); - this.flowPanelLeft.Controls.Add(this.checkBestImageQuality); - this.flowPanelLeft.Controls.Add(this.checkAnimatedAvatars); this.flowPanelLeft.Controls.Add(this.checkSmoothScrolling); this.flowPanelLeft.Controls.Add(this.labelZoom); this.flowPanelLeft.Controls.Add(this.panelZoom); + this.flowPanelLeft.Controls.Add(this.labelTweetDeckSettings); + this.flowPanelLeft.Controls.Add(this.checkBestImageQuality); + this.flowPanelLeft.Controls.Add(this.checkHideTweetsByNftUsers); + this.flowPanelLeft.Controls.Add(this.checkAnimatedAvatars); this.flowPanelLeft.Controls.Add(this.labelUpdates); this.flowPanelLeft.Controls.Add(this.checkUpdateNotifications); this.flowPanelLeft.Controls.Add(this.btnCheckUpdates); @@ -271,14 +275,37 @@ private void InitializeComponent() { // this.checkSmoothScrolling.AutoSize = true; this.checkSmoothScrolling.Font = new System.Drawing.Font("Segoe UI", 9F); - this.checkSmoothScrolling.Location = new System.Drawing.Point(6, 170); + this.checkSmoothScrolling.Location = new System.Drawing.Point(6, 122); this.checkSmoothScrolling.Margin = new System.Windows.Forms.Padding(6, 3, 3, 2); this.checkSmoothScrolling.Name = "checkSmoothScrolling"; this.checkSmoothScrolling.Size = new System.Drawing.Size(117, 19); - this.checkSmoothScrolling.TabIndex = 7; + this.checkSmoothScrolling.TabIndex = 5; this.checkSmoothScrolling.Text = "Smooth Scrolling"; this.checkSmoothScrolling.UseVisualStyleBackColor = true; // + // labelTweetDeckSettings + // + this.labelTweetDeckSettings.AutoSize = true; + this.labelTweetDeckSettings.Font = new System.Drawing.Font("Segoe UI Semibold", 10.5F, System.Drawing.FontStyle.Bold); + this.labelTweetDeckSettings.Location = new System.Drawing.Point(0, 233); + this.labelTweetDeckSettings.Margin = new System.Windows.Forms.Padding(0, 27, 0, 1); + this.labelTweetDeckSettings.Name = "labelTweetDeckSettings"; + this.labelTweetDeckSettings.Size = new System.Drawing.Size(67, 19); + this.labelTweetDeckSettings.TabIndex = 8; + this.labelTweetDeckSettings.Text = "TWITTER"; + // + // checkHideTweetsByNftUsers + // + this.checkHideTweetsByNftUsers.AutoSize = true; + this.checkHideTweetsByNftUsers.Font = new System.Drawing.Font("Segoe UI", 9F); + this.checkHideTweetsByNftUsers.Location = new System.Drawing.Point(6, 283); + this.checkHideTweetsByNftUsers.Margin = new System.Windows.Forms.Padding(6, 3, 3, 2); + this.checkHideTweetsByNftUsers.Name = "checkHideTweetsByNftUsers"; + this.checkHideTweetsByNftUsers.Size = new System.Drawing.Size(231, 19); + this.checkHideTweetsByNftUsers.TabIndex = 10; + this.checkHideTweetsByNftUsers.Text = "Hide Tweets by Users with NFT Avatars"; + this.checkHideTweetsByNftUsers.UseVisualStyleBackColor = true; + // // labelBrowserPath // this.labelBrowserPath.AutoSize = true; @@ -594,5 +621,7 @@ private void InitializeComponent() { private System.Windows.Forms.Label labelFirstDayOfWeek; private System.Windows.Forms.ComboBox comboBoxFirstDayOfWeek; private System.Windows.Forms.Label labelUI; + private System.Windows.Forms.CheckBox checkHideTweetsByNftUsers; + private System.Windows.Forms.Label labelTweetDeckSettings; } } diff --git a/Dialogs/Settings/TabSettingsGeneral.cs b/Dialogs/Settings/TabSettingsGeneral.cs index 7f608e2d..d2687e21 100644 --- a/Dialogs/Settings/TabSettingsGeneral.cs +++ b/Dialogs/Settings/TabSettingsGeneral.cs @@ -13,6 +13,7 @@ namespace TweetDuck.Dialogs.Settings { sealed partial class TabSettingsGeneral : FormSettings.BaseTab { + private readonly Action reloadTweetDeck; private readonly Action reloadColumns; private readonly UpdateChecker updates; @@ -27,9 +28,10 @@ sealed partial class TabSettingsGeneral : FormSettings.BaseTab { private readonly int searchEngineIndexDefault; private readonly int searchEngineIndexCustom; - public TabSettingsGeneral(Action reloadColumns, UpdateChecker updates) { + public TabSettingsGeneral(Action reloadTweetDeck, Action reloadColumns, UpdateChecker updates) { InitializeComponent(); + this.reloadTweetDeck = reloadTweetDeck; this.reloadColumns = reloadColumns; this.updates = updates; @@ -43,8 +45,6 @@ public TabSettingsGeneral(Action reloadColumns, UpdateChecker updates) { toolTip.SetToolTip(checkFocusDmInput, "Places cursor into Direct Message input\r\nfield when opening a conversation."); toolTip.SetToolTip(checkOpenSearchInFirstColumn, "By default, TweetDeck adds Search columns at the end.\r\nThis option makes them appear before the first column instead."); toolTip.SetToolTip(checkKeepLikeFollowDialogsOpen, "Allows liking and following from multiple accounts at once,\r\ninstead of automatically closing the dialog after taking an action."); - toolTip.SetToolTip(checkBestImageQuality, "When right-clicking a tweet image, the context menu options\r\nwill use links to the original image size (:orig in the URL)."); - toolTip.SetToolTip(checkAnimatedAvatars, "Some old Twitter avatars could be uploaded as animated GIFs."); toolTip.SetToolTip(checkSmoothScrolling, "Toggles smooth mouse wheel scrolling."); toolTip.SetToolTip(labelZoomValue, "Changes the zoom level.\r\nAlso affects notifications and screenshots."); toolTip.SetToolTip(trackBarZoom, toolTip.GetToolTip(labelZoomValue)); @@ -53,13 +53,21 @@ public TabSettingsGeneral(Action reloadColumns, UpdateChecker updates) { checkFocusDmInput.Checked = Config.FocusDmInput; checkOpenSearchInFirstColumn.Checked = Config.OpenSearchInFirstColumn; checkKeepLikeFollowDialogsOpen.Checked = Config.KeepLikeFollowDialogsOpen; - checkBestImageQuality.Checked = Config.BestImageQuality; - checkAnimatedAvatars.Checked = Config.EnableAnimatedImages; checkSmoothScrolling.Checked = Config.EnableSmoothScrolling; trackBarZoom.SetValueSafe(Config.ZoomLevel); labelZoomValue.Text = trackBarZoom.Value + "%"; + // twitter + + toolTip.SetToolTip(checkBestImageQuality, "When right-clicking a tweet image, the context menu options\r\nwill use links to the original image size (:orig in the URL)."); + toolTip.SetToolTip(checkHideTweetsByNftUsers, "Hides tweets created by users who use Twitter's NFT avatar integration.\r\nThis feature is somewhat experimental, some accounts might not be detected immediately."); + toolTip.SetToolTip(checkAnimatedAvatars, "Some old Twitter avatars could be uploaded as animated GIFs."); + + checkBestImageQuality.Checked = Config.BestImageQuality; + checkHideTweetsByNftUsers.Checked = Config.HideTweetsByNftUsers; + checkAnimatedAvatars.Checked = Config.EnableAnimatedImages; + // updates toolTip.SetToolTip(checkUpdateNotifications, "Checks for updates every hour.\r\nIf an update is dismissed, it will not appear again."); @@ -135,11 +143,13 @@ public override void OnReady() { checkFocusDmInput.CheckedChanged += checkFocusDmInput_CheckedChanged; checkOpenSearchInFirstColumn.CheckedChanged += checkOpenSearchInFirstColumn_CheckedChanged; checkKeepLikeFollowDialogsOpen.CheckedChanged += checkKeepLikeFollowDialogsOpen_CheckedChanged; - checkBestImageQuality.CheckedChanged += checkBestImageQuality_CheckedChanged; - checkAnimatedAvatars.CheckedChanged += checkAnimatedAvatars_CheckedChanged; checkSmoothScrolling.CheckedChanged += checkSmoothScrolling_CheckedChanged; trackBarZoom.ValueChanged += trackBarZoom_ValueChanged; + checkBestImageQuality.CheckedChanged += checkBestImageQuality_CheckedChanged; + checkHideTweetsByNftUsers.CheckedChanged += checkHideTweetsByNftUsers_CheckedChanged; + checkAnimatedAvatars.CheckedChanged += checkAnimatedAvatars_CheckedChanged; + checkUpdateNotifications.CheckedChanged += checkUpdateNotifications_CheckedChanged; btnCheckUpdates.Click += btnCheckUpdates_Click; @@ -177,15 +187,6 @@ private void checkKeepLikeFollowDialogsOpen_CheckedChanged(object sender, EventA Config.KeepLikeFollowDialogsOpen = checkKeepLikeFollowDialogsOpen.Checked; } - private void checkBestImageQuality_CheckedChanged(object sender, EventArgs e) { - Config.BestImageQuality = checkBestImageQuality.Checked; - } - - private void checkAnimatedAvatars_CheckedChanged(object sender, EventArgs e) { - Config.EnableAnimatedImages = checkAnimatedAvatars.Checked; - BrowserProcessHandler.UpdatePrefs().ContinueWith(task => reloadColumns()); - } - private void checkSmoothScrolling_CheckedChanged(object sender, EventArgs e) { Config.EnableSmoothScrolling = checkSmoothScrolling.Checked; } @@ -205,6 +206,24 @@ private void zoomUpdateTimer_Tick(object sender, EventArgs e) { #endregion + #region Twitter + + private void checkBestImageQuality_CheckedChanged(object sender, EventArgs e) { + Config.BestImageQuality = checkBestImageQuality.Checked; + } + + private void checkHideTweetsByNftUsers_CheckedChanged(object sender, EventArgs e) { + Config.HideTweetsByNftUsers = checkHideTweetsByNftUsers.Checked; + BeginInvoke(reloadTweetDeck); + } + + private void checkAnimatedAvatars_CheckedChanged(object sender, EventArgs e) { + Config.EnableAnimatedImages = checkAnimatedAvatars.Checked; + BrowserProcessHandler.UpdatePrefs().ContinueWith(task => reloadColumns()); + } + + #endregion + #region Updates private void checkUpdateNotifications_CheckedChanged(object sender, EventArgs e) { diff --git a/Resources/Content/.all.js b/Resources/Content/.all.js index 5747e563..5b73041d 100644 --- a/Resources/Content/.all.js +++ b/Resources/Content/.all.js @@ -45,6 +45,7 @@ import limit_loaded_dm_count from "./tweetdeck/limit_loaded_dm_count.js"; import make_retweets_lowercase from "./tweetdeck/make_retweets_lowercase.js"; import middle_click_tweet_icon_actions from "./tweetdeck/middle_click_tweet_icon_actions.js"; import move_accounts_above_hashtags_in_search from "./tweetdeck/move_accounts_above_hashtags_in_search.js"; +import mute_accounts_with_nft_avatars from "./tweetdeck/mute_accounts_with_nft_avatars.js"; import offline_notification from "./tweetdeck/offline_notification.js"; import open_search_externally from "./tweetdeck/open_search_externally.js"; import open_search_in_first_column from "./tweetdeck/open_search_in_first_column.js"; diff --git a/Resources/Content/api/bridge.js b/Resources/Content/api/bridge.js index 93cdfa99..a7aedf28 100644 --- a/Resources/Content/api/bridge.js +++ b/Resources/Content/api/bridge.js @@ -37,6 +37,7 @@ if (!("$TDX" in window)) { * @property {boolean} [expandLinksOnHover] * @property {number} [firstDayOfWeek] * @property {boolean} [focusDmInput] + * @property {boolean} [hideTweetsByNftUsers] * @property {boolean} [keepLikeFollowDialogsOpen] * @property {boolean} [muteNotifications] * @property {boolean} [notificationMediaPreviews] diff --git a/Resources/Content/api/td.js b/Resources/Content/api/td.js index 0ff2757a..791e6853 100644 --- a/Resources/Content/api/td.js +++ b/Resources/Content/api/td.js @@ -40,6 +40,7 @@ if (!("TD" in window)) { * @property {TD_Column_Model} model * @property {boolean} notificationsDisabled * @property {function} reloadTweets + * @property {ChirpBase[]} updateArray * @property {{ columnWidth: number }} visibility */ @@ -272,10 +273,14 @@ if (!("TD" in window)) { * @typedef TwitterClient * @type {Object} * + * @property {string} API_BASE_URL + * @property {function(id: string)} addIdToMuteList * @property {function(chirp: ChirpBase)} callback * @property {string} chirpId * @property {TwitterConversations} conversations * @property {function(ids: string[], onSuccess: function(users: TwitterUser[]), onError: function)} getUsersByIds + * @property {function(url: string, data: object, method: "GET"|"POST", responseProcessor: function, onSuccess: function, onError: function)} makeTwitterCall + * @property {function(json: string[]): TwitterUser[]} processUsers */ /** @@ -360,6 +365,7 @@ if (!("TD" in window)) { * @typedef TwitterUserJSON * @type {Object} * + * @property {boolean} [ext_has_nft_avatar] * @property {string} id * @property {string} id_str * @property {string} name diff --git a/Resources/Content/tweetdeck/globals/user_nft_status.js b/Resources/Content/tweetdeck/globals/user_nft_status.js new file mode 100644 index 00000000..f71f0675 --- /dev/null +++ b/Resources/Content/tweetdeck/globals/user_nft_status.js @@ -0,0 +1,210 @@ +import { TD } from "../../api/td.js"; +import { checkPropertyExists, noop } from "../../api/utils.js"; + +function isSupported() { + return checkPropertyExists(TD, "controller", "clients", "getPreferredClient") && + checkPropertyExists(TD, "services", "TwitterClient", "prototype", "API_BASE_URL") && + checkPropertyExists(TD, "services", "TwitterClient", "prototype", "makeTwitterCall") && + checkPropertyExists(TD, "services", "TwitterClient", "prototype", "processUsers") && + checkPropertyExists(TD, "services", "TwitterUser", "prototype"); +} + +/** + * @type {function(id: string)[]} + */ +const nftUserListeners = []; + +/** + * @type {Map<string, boolean>} + */ +const knownStatus = new Map(); + +/** + * @type {Map<string, function(result: boolean)[]>} + */ +const deferredCallbacks = new Map(); + +/** + * @type {Set<string>} + */ +const usersInQueue = new Set(); + +/** + * @type {Set<string>} + */ +const usersPending = new Set(); + +let checkTimer = -1; + +function requestQueuedUserInfo() { + if (usersInQueue.size === 0) { + return; + } + + const ids = []; + + for (const id of usersInQueue) { + if (ids.length === 100) { + break; + } + + ids.push(id); + usersInQueue.delete(id); + } + + // noinspection JSUnusedGlobalSymbols + const data = { + user_id: ids.join(",") + }; + + /** + * @param {TwitterUserJSON[]} users + */ + const processUserData = function(users) { + for (const user of users) { + setUserNftStatus(user.id_str, user.ext_has_nft_avatar === true); + } + }; + + const client = TD.controller.clients.getPreferredClient(); + + client.makeTwitterCall(client.API_BASE_URL + "users/lookup.json?include_ext_has_nft_avatar=1", data, "POST", processUserData, noop, function() { + // In case of API error, assume the users are not associated with NFTs so that callbacks can fire. + for (const id of ids) { + setUserNftStatus(id, false); + } + }); + + if (usersInQueue.size === 0) { + checkTimer = -1; + } + else { + checkTimer = window.setTimeout(requestQueuedUserInfo, 400); + } +} + +/** + * Calls the provided callback function with the result of whether a user id is associated with NFTs. + * If the user id is null, it will be presumed as not associated with NFTs. + * @param {string|null} id + * @param {function(nft: boolean)} [callback] + */ +function checkUserNftStatusCallback(id, callback) { + if (id === null) { + callback && callback(false); + return; + } + + const status = knownStatus.get(id); + + if (status !== undefined) { + callback && callback(status); + return; + } + + if (callback) { + let callbackList = deferredCallbacks.get(id); + if (callbackList === undefined) { + deferredCallbacks.set(id, callbackList = []); + } + + callbackList.push(callback); + } + + if (usersPending.has(id)) { + return; + } + + usersInQueue.add(id); + usersPending.add(id); + + window.clearTimeout(checkTimer); + checkTimer = window.setTimeout(requestQueuedUserInfo, 400); +} + +/** + * Checks whether a user id is associated with NFTs, but only using already known results. + * If the user id is null or has not been checked yet, it will be presumed as not associated with NFTs. + * @param {string|null} id + * @return {boolean} + */ +export function checkUserNftStatusImmediately(id) { + return id !== null && knownStatus.get(id) === true; +} + +/** + * Adds a listener that gets called when a user is added to the list of users associated with NFTs. + * If some users were already known to be associated with NFTs before registering the listener, the listener will be called for every user. + * @param {function(id: string)} listener + */ +export function addNftUserListener(listener) { + nftUserListeners.push(listener); + + for (const entry of knownStatus.entries()) { + if (entry[1]) { + listener(entry[0]); + } + } +} + +/** + * Sets whether a user id is associated with NFTs. + * @param {string} id + * @param {boolean} status + */ +export function setUserNftStatus(id, status) { + usersInQueue.delete(id); + usersPending.delete(id); + + if (knownStatus.get(id) !== status) { + knownStatus.set(id, status); + + if (status) { + for (const listener of nftUserListeners) { + try { + listener(id); + } catch (e) { + console.error("Error in NFT user listener: " + e); + } + } + } + } + + if (deferredCallbacks.has(id)) { + for (const callback of deferredCallbacks.get(id)) { + callback(status); + } + + deferredCallbacks.delete(id); + } +} + +/** + * Calls the provided callback function with the result of whether a user id is associated with NFTs. + * @param {string} id + * @param {function(nft: boolean)} [callback] + */ +export const checkUserNftStatus = isSupported() ? checkUserNftStatusCallback : function(id, callback) { + callback && callback(false); +}; + +/** + * Utility function that returns the user id from a tweet. + * @param {ChirpBase} tweet + * @returns {string|null} + */ +export function getTweetUserId(tweet) { + const user = tweet.user; + return typeof user === "object" && typeof user.id === "string" ? user.id : null; +} + +/** + * Clears known status of users who are not associated with NFTs, in case they became associated with NFTs in the meantime. + */ +window.setInterval(function() { + for (const entry of knownStatus.entries()) { + if (!entry[1]) { + knownStatus.delete(entry[0]); + } + } +}, 1000 * 60 * 60); diff --git a/Resources/Content/tweetdeck/mute_accounts_with_nft_avatars.js b/Resources/Content/tweetdeck/mute_accounts_with_nft_avatars.js new file mode 100644 index 00000000..73ac777f --- /dev/null +++ b/Resources/Content/tweetdeck/mute_accounts_with_nft_avatars.js @@ -0,0 +1,54 @@ +import { $TDX } from "../api/bridge.js"; +import { replaceFunction } from "../api/patch.js"; +import { TD } from "../api/td.js"; +import { ensurePropertyExists } from "../api/utils.js"; +import { addNftUserListener, checkUserNftStatus, checkUserNftStatusImmediately, getTweetUserId, setUserNftStatus } from "./globals/user_nft_status.js"; + +export default function() { + if (!$TDX.hideTweetsByNftUsers) { + return; + } + + ensurePropertyExists(TD, "controller", "clients", "getPreferredClient"); + ensurePropertyExists(TD, "services", "TwitterClient", "prototype", "addIdToMuteList"); + ensurePropertyExists(TD, "services", "TwitterUser", "prototype"); + ensurePropertyExists(TD, "vo", "Column", "prototype", "addItemsToIndex"); + + addNftUserListener(function(id) { + TD.controller.clients.getPreferredClient().addIdToMuteList(id); + }); + + replaceFunction(TD.services.TwitterUser.prototype, "fromJSONObject", function(func, args) { + /** @type {TwitterUser} */ + const user = func.apply(this, args); + + if (args.length > 0 && typeof args[0] === "object") { + const id = user.id; + const json = args[0]; + + if ("ext_has_nft_avatar" in json) { + setUserNftStatus(id, json.ext_has_nft_avatar === true); + } + else { + checkUserNftStatus(id); + } + } + + return user; + }); + + replaceFunction(TD.vo.Column.prototype, "mergeAndRenderChirps", function(func, args) { + /** @type ChirpBase[] */ + const tweets = args[0]; + + if (Array.isArray(tweets)) { + for (let i = tweets.length - 1; i >= 0; i--) { + if (checkUserNftStatusImmediately(getTweetUserId(tweets[i]))) { + tweets.splice(i, 1); + } + } + } + + return func.apply(this, args); + }); +}; diff --git a/Resources/Content/tweetdeck/setup_desktop_notifications.js b/Resources/Content/tweetdeck/setup_desktop_notifications.js index fb5c558c..e8e2b0dc 100644 --- a/Resources/Content/tweetdeck/setup_desktop_notifications.js +++ b/Resources/Content/tweetdeck/setup_desktop_notifications.js @@ -4,6 +4,7 @@ import { replaceFunction } from "../api/patch.js"; import { TD } from "../api/td.js"; import { checkPropertyExists, ensurePropertyExists } from "../api/utils.js"; import { getColumnName } from "./globals/get_column_name.js"; +import { checkUserNftStatus, getTweetUserId } from "./globals/user_nft_status.js"; /** * Event callback for a new tweet. @@ -67,20 +68,7 @@ const onNewTweet = (function() { * @param {TD_Column} column * @param {ChirpBase} tweet */ - return function(column, tweet) { - if (tweet instanceof TD.services.TwitterConversation || tweet instanceof TD.services.TwitterConversationMessageEvent) { - if (checkTweetCache(recentMessages, tweet.id)) { - return; - } - } - else { - if (checkTweetCache(recentTweets, tweet.id)) { - return; - } - } - - startRecentTweetTimer(); - + const showTweetNotification = function(column, tweet) { if (column.model.getHasNotification()) { const sensitive = isSensitive(tweet); const previews = $TDX.notificationMediaPreviews && (!sensitive || TD.settings.getDisplaySensitiveMedia()); @@ -184,6 +172,36 @@ const onNewTweet = (function() { $TD.onTweetSound(); } }; + + /** + * @param {TD_Column} column + * @param {ChirpBase} tweet + */ + return function(column, tweet) { + if (tweet instanceof TD.services.TwitterConversation || tweet instanceof TD.services.TwitterConversationMessageEvent) { + if (checkTweetCache(recentMessages, tweet.id)) { + return; + } + } + else { + if (checkTweetCache(recentTweets, tweet.id)) { + return; + } + } + + startRecentTweetTimer(); + + if (!$TDX.hideTweetsByNftUsers) { + showTweetNotification(column, tweet); + } + else { + checkUserNftStatus(getTweetUserId(tweet), function(nft) { + if (!nft) { + showTweetNotification(column, tweet); + } + }); + } + }; })(); /** diff --git a/TweetDuck.csproj b/TweetDuck.csproj index 3ff80c8e..6a5fa797 100644 --- a/TweetDuck.csproj +++ b/TweetDuck.csproj @@ -402,6 +402,7 @@ <None Include="Resources\Content\tweetdeck\globals\reload_columns.js" /> <None Include="Resources\Content\tweetdeck\globals\retrieve_tweet.js" /> <None Include="Resources\Content\tweetdeck\globals\show_tweet_detail.js" /> + <None Include="Resources\Content\tweetdeck\globals\user_nft_status.js" /> <None Include="Resources\Content\tweetdeck\handle_extra_mouse_buttons.js" /> <None Include="Resources\Content\tweetdeck\hook_theme_settings.js" /> <None Include="Resources\Content\tweetdeck\inject_css.js" /> @@ -410,6 +411,7 @@ <None Include="Resources\Content\tweetdeck\make_retweets_lowercase.js" /> <None Include="Resources\Content\tweetdeck\middle_click_tweet_icon_actions.js" /> <None Include="Resources\Content\tweetdeck\move_accounts_above_hashtags_in_search.js" /> + <None Include="Resources\Content\tweetdeck\mute_accounts_with_nft_avatars.js" /> <None Include="Resources\Content\tweetdeck\offline_notification.js" /> <None Include="Resources\Content\tweetdeck\open_search_externally.js" /> <None Include="Resources\Content\tweetdeck\open_search_in_first_column.js" /> diff --git a/lib/TweetLib.Core/Application/IAppUserConfiguration.cs b/lib/TweetLib.Core/Application/IAppUserConfiguration.cs index c65e68da..c9c4ae93 100644 --- a/lib/TweetLib.Core/Application/IAppUserConfiguration.cs +++ b/lib/TweetLib.Core/Application/IAppUserConfiguration.cs @@ -9,6 +9,7 @@ public interface IAppUserConfiguration { bool ExpandLinksOnHover { get; } bool FirstRun { get; } bool FocusDmInput { get; } + bool HideTweetsByNftUsers { get; } bool IsCustomSoundNotificationSet { get; } bool KeepLikeFollowDialogsOpen { get; } bool MuteNotifications { get; } diff --git a/lib/TweetLib.Core/Features/PropertyObjectScript.cs b/lib/TweetLib.Core/Features/PropertyObjectScript.cs index 5ac6186e..946145f4 100644 --- a/lib/TweetLib.Core/Features/PropertyObjectScript.cs +++ b/lib/TweetLib.Core/Features/PropertyObjectScript.cs @@ -13,7 +13,7 @@ internal static string Generate(IAppUserConfiguration config, Environment enviro static string Bool(bool value) => value ? "true;" : "false;"; static string Str(string value) => $"\"{value}\";"; - StringBuilder build = new StringBuilder(384).Append("(function(x){"); + StringBuilder build = new StringBuilder(414).Append("(function(x){"); build.Append("x.expandLinksOnHover=").Append(Bool(config.ExpandLinksOnHover)); @@ -24,6 +24,7 @@ internal static string Generate(IAppUserConfiguration config, Environment enviro build.Append("x.muteNotifications=").Append(Bool(config.MuteNotifications)); build.Append("x.notificationMediaPreviews=").Append(Bool(config.NotificationMediaPreviews)); build.Append("x.translationTarget=").Append(Str(config.TranslationTarget)); + build.Append("x.hideTweetsByNftUsers=").Append(Bool(config.HideTweetsByNftUsers)); build.Append("x.firstDayOfWeek=").Append(config.CalendarFirstDay == -1 ? JQuery.GetDatePickerDayOfWeek(Lib.Culture.DateTimeFormat.FirstDayOfWeek) : config.CalendarFirstDay); } diff --git a/lib/TweetLib.Core/Features/TweetDeck/TweetDeckBrowser.cs b/lib/TweetLib.Core/Features/TweetDeck/TweetDeckBrowser.cs index d20b19a5..71c1c838 100644 --- a/lib/TweetLib.Core/Features/TweetDeck/TweetDeckBrowser.cs +++ b/lib/TweetLib.Core/Features/TweetDeck/TweetDeckBrowser.cs @@ -249,6 +249,9 @@ private sealed class ResourceRequestHandler : BaseResourceRequestHandler { case ResourceType.Xhr when url.Contains(UrlVersionCheck): return RequestHandleResult.Cancel.Instance; + case ResourceType.Xhr when url.Contains("://api.twitter.com/") && url.Contains("include_entities=1") && !url.Contains("&include_ext_has_nft_avatar=1"): + return new RequestHandleResult.Redirect(url.Replace("include_entities=1", "include_entities=1&include_ext_has_nft_avatar=1")); + default: return base.Handle(url, resourceType); }