1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-04-24 06:15:48 +02:00

Add option to hide tweets by users with NFT avatars

This commit is contained in:
chylex 2022-01-21 04:48:00 +01:00
parent 4751a948e7
commit 1ced72388b
Signed by: chylex
GPG Key ID: 4DE42C8F19A80548
14 changed files with 399 additions and 53 deletions

View File

@ -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;

View File

@ -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());

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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";

View File

@ -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]

View File

@ -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

View File

@ -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);

View File

@ -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);
});
};

View File

@ -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);
}
});
}
};
})();
/**

View File

@ -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" />

View File

@ -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; }

View File

@ -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);
}

View File

@ -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);
}