1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-05-06 23:34:05 +02:00

Refactor main browser code into JS modules

This commit is contained in:
chylex 2021-12-18 08:50:42 +01:00
parent 7239dcf4d2
commit ed4f7b6b72
Signed by: chylex
GPG Key ID: 4DE42C8F19A80548
79 changed files with 3284 additions and 1935 deletions
.idea/.idea.TweetDuck/.idea
Browser
Resources
Content
.all.js
api
bootstrap.js
features
globals
Plugins/edit-design
Scripts
TweetDuck.csproj

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="PROJECT" libraries="{@types/jquery}" />
</component>
</project>

View File

@ -33,6 +33,10 @@ private TweetDeckBridge(FormBrowser form, FormNotificationMain notification) {
public sealed class Browser : TweetDeckBridge {
public Browser(FormBrowser form, FormNotificationMain notification) : base(form, notification) {}
public void OnFeaturesLoaded() {
form.InvokeAsyncSafe(form.OnFeaturesLoaded);
}
public void OpenContextMenu() {
form.InvokeAsyncSafe(form.OpenContextMenu);
}

View File

@ -368,6 +368,10 @@ protected override void WndProc(ref Message m) {
// bridge methods
public void OnFeaturesLoaded() {
browser.OnFeaturesLoaded();
}
public void PauseNotification() {
notification.PauseNotification();
}

View File

@ -156,11 +156,7 @@ private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e) {
if (frame.IsMain) {
if (TwitterUrls.IsTweetDeck(url)) {
UpdateProperties();
CefScriptExecutor.RunFile(frame, "code.js");
InjectBrowserCSS();
ReinjectCustomCSS(Config.CustomBrowserCSS);
Config_SoundNotificationInfoChanged(null, EventArgs.Empty);
CefScriptExecutor.RunFile(frame, "bootstrap.tweetdeck.js");
TweetDeckBridge.ResetStaticProperties();
@ -239,6 +235,12 @@ public void ReloadToTweetDeck() {
browser.ExecuteJsAsync($"if(window.TDGF_reload)window.TDGF_reload();else window.location.href='{TwitterUrls.TweetDeck}'");
}
public void OnFeaturesLoaded() {
InjectBrowserCSS();
ReinjectCustomCSS(Config.CustomBrowserCSS);
Config_SoundNotificationInfoChanged(null, EventArgs.Empty);
}
public void UpdateProperties() {
browser.ExecuteJsAsync(PropertyBridge.GenerateScript(PropertyBridge.Environment.Browser));
}

74
Resources/Content/.all.js Normal file
View File

@ -0,0 +1,74 @@
// noinspection ES6UnusedImports,JSUnusedLocalSymbols
/*
* Imports all root modules and global functions to mark them as used.
*/
import add_tweetduck_to_settings_menu from "./features/add_tweetduck_to_settings_menu.js";
import bypass_tco_links from "./features/bypass_t.co_links.js";
import clear_search_input from "./features/clear_search_input.js";
import configure_first_day_of_week from "./features/configure_first_day_of_week.js";
import configure_language_for_translations from "./features/configure_language_for_translations.js";
import disable_clipboard_formatting from "./features/disable_clipboard_formatting.js";
import disable_td_metrics from "./features/disable_td_metrics.js";
import drag_links_onto_columns from "./features/drag_links_onto_columns.js";
import expand_links_or_show_tooltip from "./features/expand_links_or_show_tooltip.js";
import fix_dm_input_box_focus from "./features/fix_dm_input_box_focus.js";
import fix_horizontal_scrolling_of_column_container from "./features/fix_horizontal_scrolling_of_column_container.js";
import fix_marking_dm_as_read_when_replying from "./features/fix_marking_dm_as_read_when_replying.js";
import fix_media_preview_urls from "./features/fix_media_preview_urls.js";
import fix_missing_bing_translator_languages from "./features/fix_missing_bing_translator_languages.js";
import fix_os_name from "./features/fix_os_name.js";
import fix_scheduled_tweets_not_appearing from "./features/fix_scheduled_tweets_not_appearing.js";
import fix_youtube_previews from "./features/fix_youtube_previews.js";
import focus_composer_after_alt_tab from "./features/focus_composer_after_alt_tab.js";
import focus_composer_after_image_upload from "./features/focus_composer_after_image_upload.js";
import focus_composer_after_switching_account from "./features/focus_composer_after_switching_account.js";
import handle_extra_mouse_buttons from "./features/handle_extra_mouse_buttons.js";
import hook_theme_settings from "./features/hook_theme_settings.js";
import inject_css from "./features/inject_css.js";
import keep_like_follow_dialogs_open from "./features/keep_like_follow_dialogs_open.js";
import limit_loaded_dm_count from "./features/limit_loaded_dm_count.js";
import make_retweets_lowercase from "./features/make_retweets_lowercase.js";
import middle_click_tweet_icon_actions from "./features/middle_click_tweet_icon_actions.js";
import move_accounts_above_hashtags_in_search from "./features/move_accounts_above_hashtags_in_search.js";
import offline_notification from "./features/offline_notification.js";
import open_search_externally from "./features/open_search_externally.js";
import open_search_in_first_column from "./features/open_search_in_first_column.js";
import paste_images_from_clipboard from "./features/paste_images_from_clipboard.js";
import perform_search from "./features/perform_search.js";
import pin_composer_icon from "./features/pin_composer_icon.js";
import ready_plugins from "./features/ready_plugins.js";
import register_composer_active_event from "./features/register_composer_active_event.js";
import register_global_functions from "./features/register_global_functions.js";
import restore_cleared_column from "./features/restore_cleared_column.js";
import screenshot_tweet from "./features/screenshot_tweet.js";
import setup_column_type_attributes from "./features/setup_column_type_attributes.js";
import setup_desktop_notifications from "./features/setup_desktop_notifications.js";
import setup_link_context_menu from "./features/setup_link_context_menu.js";
import setup_sound_notifications from "./features/setup_sound_notifications.js";
import setup_tweet_context_menu from "./features/setup_tweet_context_menu.js";
import setup_tweetduck_account_bamboozle from "./features/setup_tweetduck_account_bamboozle.js";
import setup_video_player from "./features/setup_video_player.js";
import skip_pre_login_page from "./features/skip_pre_login_page.js";
const globalFunctions = [
window.TDGF_applyROT13,
window.TDGF_getColumnName,
window.TDGF_injectBrowserCSS,
window.TDGF_injectMustache,
window.TDGF_onGlobalDragStart,
window.TDGF_onMouseClickExtra,
window.TDGF_onPropertiesUpdated,
window.TDGF_performSearch,
window.TDGF_playSoundNotification,
window.TDGF_playVideo,
window.TDGF_registerPropertyUpdateCallback,
window.TDGF_reinjectCustomCSS,
window.TDGF_reload,
window.TDGF_reloadColumns,
window.TDGF_setSoundNotificationData,
window.TDGF_showTweetDetail,
window.TDGF_triggerScreenshot,
window.jQuery,
];

View File

@ -0,0 +1,71 @@
if (!("$TD" in window)) {
throw "Missing $TD in global scope";
}
if (!("$TDX" in window)) {
throw "Missing $TDX in global scope";
}
/**
* @typedef TD_Bridge
* @type {Object}
*
* @property {function(message: string)} crashDebug
* @property {function(tooltip: string|null)} displayTooltip
* @property {function} fixClipboard
* @property {function(fontSize: string, headLayout: string)} loadNotificationLayout
* @property {function} onFeaturesLoaded
* @property {function(columnId: string, chirpId: string, columnName: string, tweetHtml: string, tweetCharacters: int, tweetUrl: string, quoteUrl: string)} onTweetPopup
* @property {function} onTweetSound
* @property {function(url: string)} openBrowser
* @property {function} openContextMenu
* @property {function(videoUrl: string, tweetUrl: string, username: string, callback: function)} playVideo
* @property {function(tweetUrl: string, quoteUrl: string, chirpAuthors: string, chirpImages: string)} setRightClickedChirp
* @property {function(type: string, url: string)} setRightClickedLink
* @property {function(html: string, width: number)} screenshotTweet
* @property {function} stopVideo
*/
/**
* @typedef TD_Properties
* @type {Object}
*
* @property {boolean} [expandLinksOnHover]
* @property {number} [firstDayOfWeek]
* @property {boolean} [focusDmInput]
* @property {boolean} [keepLikeFollowDialogsOpen]
* @property {boolean} [muteNotifications]
* @property {boolean} [notificationMediaPreviews]
* @property {boolean} [openSearchInFirstColumn]
* @property {string} [translationTarget]
*/
/** @type {TD_Bridge} */
export const $TD = window.$TD;
/** @type {TD_Properties} */
export const $TDX = window.$TDX;
/**
* @type {function(TD_Properties)[]}
*/
const propertyUpdateCallbacks = [];
/**
* Registers a callback that responds to `$TDX` property value changes.
* @param {function(TD_Properties)} callback
* @param {boolean} [callAfterRegistering] whether to call the callback immediately after registering
*/
export function registerPropertyUpdateCallback(callback, callAfterRegistering) {
propertyUpdateCallbacks.push(callback);
if (callAfterRegistering) {
callback($TDX);
}
}
/**
* Triggers all registered callbacks.
*/
export function triggerPropertiesUpdated() {
propertyUpdateCallbacks.forEach(func => func($TDX));
}

67
Resources/Content/api/jquery.js vendored Normal file
View File

@ -0,0 +1,67 @@
if (!("$" in window)) {
throw "Missing jQuery in global scope";
}
/** @type {JQuery} */
export const $ = window.$;
/**
* Returns a jQuery object, or throws if no elements are found.
* @param {JQuery.Selector} selector
* @param {Element|string|null} [context]
* @returns {JQuery}
*/
export const $$ = function(selector, context) {
// noinspection JSValidateTypes
const result = $(selector, context);
if (!result.length) {
throw "No elements were found for selector: " + selector;
}
return result;
};
/**
* @typedef InternalJQueryData
* @type {Object}
*
* @property {InternalJQueryEvents} events
*/
/**
* @typedef InternalJQueryEvents
* @type {Object<string, function[]>}
*/
/**
* @param {EventTarget} target
* @returns {InternalJQueryEvents}
*/
export function getEvents(target) {
// noinspection JSUnresolvedFunction
/** @type {InternalJQueryData} */
const jqData = $._data(target);
return jqData.events;
}
/**
* Throws if an element does not have a registered jQuery event.
* @param {EventTarget|HTMLElement} element
* @param {string} eventName
*/
export function ensureEventExists(element, eventName) {
if (!(eventName in getEvents(element))) {
const tagName = element?.tagName.toLowerCase() ?? element?.nodeName;
const classList = "classList" in element ? Array.from(element.classList).map(cls => `.${cls}`).join("") : "";
throw `Missing jQuery event '${eventName}' in: ${tagName}${classList}`;
}
}
/**
* @param jq {JQuery}
* @returns {HTMLElement|null}
*/
export function single(jq) {
return jq.length === 1 ? jq[0] : null;
}

View File

@ -0,0 +1,41 @@
import { $ } from "./jquery.js";
import { TD } from "./td.js";
import { crashDebug } from "./utils.js";
const callbacks = [];
/**
* @param {function} callback
*/
function executeCallback(callback) {
try {
callback();
} catch (e) {
crashDebug("Caught error in function " + callback.name);
console.error(e);
}
}
/**
* @returns {boolean}
*/
export function isAppReady() {
return TD.ready;
}
/**
* @param {function} callback
*/
export function onAppReady(callback) {
if (isAppReady()) {
executeCallback(callback);
}
else {
callbacks.push(callback);
}
}
$(document).one("TD.ready", function() {
callbacks.forEach(executeCallback);
callbacks.length = 0;
});

375
Resources/Content/api/td.js Normal file
View File

@ -0,0 +1,375 @@
if (!("TD" in window)) {
throw "Missing TD in global scope";
}
/**
* @typedef TD
* @type {Object}
*
* @property {TD_Clients} clients
* @property {TD_Components} components
* @property {TD_Controller} controller
* @property {TD_Languages} languages
* @property {TD_Metrics} metrics
* @property {Map<string, string>|null} mustaches
* @property {TD_Services} services
* @property {TD_Settings} settings
* @property {TD_UI} ui
* @property {TD_Util} util
* @property {TD_VO} vo
*
* @property {boolean} ready
*/
/**
* @typedef TD_Clients
* @type {Object}
*
* @property {function(key: string): TwitterClient|null} getClient
* @property {function(): TwitterClient} getPreferredClient
*/
/**
* @typedef TD_Column
* @type {Object}
*
* @property {string} [_tduck_icon]
* @property {function(id: string): ChirpBase|null} findChirp
* @property {function(id: string): ChirpBase|null} findMostInterestingChirp
* @property {function: string} getMediaPreviewSize
* @property {TD_Column_Model} model
* @property {boolean} notificationsDisabled
* @property {function} reloadTweets
*/
/**
* @typedef TD_Column_Model
* @type {Object}
*
* @property {function: boolean} getHasNotification
* @property {function: boolean} getHasSound
* @property {function: string} getKey
* @property {{ key: string, apiid: string }} privateState
* @property {function(timestamp: number)} setClearedTimestamp
*/
/**
* @typedef TD_Components
* @type {Object}
*
* @property {Class<BaseModal>} BaseModal
* @property {Class} ConversationDetailView
* @property {Class<MediaGallery>} MediaGallery
*/
/**
* @typedef TD_Controller
* @type {Object}
*
* @property {TD_Controller_ColumnManager} columnManager
* @property {TD_Controller_Init} init
* @property {TD_Controller_Notifications} notifications
* @property {TD_Controller_Stats} stats
*/
/**
* @typedef TD_Controller_ColumnManager
* @type {Object}
*
* @property {string[]} _columnOrder
* @property {function(id: string): TD_Column|null} get
* @property {function: Map<string, TD_Column>} getAll
* @property {function: TD_Column[]} getAllOrdered
* @property {function(id: string): TD_Column|null} getByApiid
* @property {function(id: string, direction: "left")} move
* @property {function(modelKey: string)} showColumn
*/
/**
* @typedef TD_Controller_Init
* @type {Object}
*
* @property {function} showLogin
*/
/**
* @typedef TD_Controller_Notifications
* @type {Object}
*
* @property {function: boolean} hasNotifications
* @property {function: boolean} isPermissionGranted
*/
/**
* @typedef TD_Controller_Stats
* @type {Object}
*
* @property {function} quoteTweet
*/
/**
* @typedef TD_Languages
* @type {Object}
*
* @property {function: string[]} getSupportedTranslationSourceLanguages
*/
/**
* @typedef TD_Metrics
* @type {Object}
*
* @property {function} inflate
* @property {function} inflateMetricTriple
* @property {function} log
* @property {function} makeKey
* @property {function} send
*/
/**
* @typedef TD_Services
* @type {Object}
*
* @property {ChirpBase_Class} ChirpBase
* @property {TwitterActionFollow_Class} TwitterActionFollow
* @property {Class} TwitterActionRetweet
* @property {Class} TwitterActionRetweetedInteraction
* @property {Class<TwitterClient>} TwitterClient
* @property {Class<TwitterConversation>} TwitterConversation
* @property {Class} TwitterConversationMessageEvent
* @property {TwitterMedia_Class} TwitterMedia
* @property {Class<TwitterStatus>} TwitterStatus
* @property {Class<TwitterUser>} TwitterUser
*/
/**
* @typedef TD_Settings
* @type {Object}
*
* @property {function: boolean} getComposeStayOpen
* @property {function: boolean} getDisplaySensitiveMedia
* @property {function: string} getFontSize
* @property {function: string} getTheme
* @property {function(boolean)} setComposeStayOpen
* @property {function(string)} setFontSize
* @property {function(string)} setTheme
*/
/**
* @typedef TD_UI
* @type {Object}
*
* @property {Object} columns
* @property {TD_UI_Updates} updates
*/
/**
* @typedef TD_UI_Updates
* @type {Object}
*
* @property {function(column: TD_Column, chirp: ChirpBase, parentChirp: ChirpBase)} showDetailView
*/
/**
* @typedef TD_Util
* @type {Object}
*
* @property {function(a: any, b: any): number} chirpReverseColumnSort
* @property {function} getOSName
*/
/**
* @typedef TD_VO
* @type {Object}
*
* @property {Class} Column
*/
/**
* @typedef BaseModal
* @typedef {Object}
*/
/**
* @typedef ChirpBase
* @type {Object}
*
* @property {TwitterUser_Account} account
* @property {string} chirpType
* @property {function: string} getChirpType
* @property {function: string} getChirpURL
* @property {function: ChirpBase|null} getMainTweet
* @property {function: TwitterUser} getMainUser
* @property {function: TwitterMedia[]} getMedia
* @property {function: ChirpBase|null} getRelatedTweet
* @property {function: TwitterUser[]} getReplyUsers
* @property {function: boolean} hasImage
* @property {function: boolean} hasMedia
* @property {string} id
* @property {function: boolean} isAboutYou
* @property {TwitterConversationMessageEvent[]} [messages]
* @property {boolean} possiblySensitive
* @property {ChirpBase|null} quotedTweet
* @property {function(ChirpRenderSettings)} render
* @property {TwitterUser} user
*/
/**
* @typedef ChirpBase_Class
* @type {Class<ChirpBase>}
*
* @property {string} TWEET
*/
/**
* @typedef ChirpRenderSettings
* @type {Object}
*
* @property {boolean} withFooter
* @property {boolean} withTweetActions
* @property {boolean} isInConvo
* @property {boolean} isFavorite
* @property {boolean} isRetweeted
* @property {boolean} isPossiblySensitive
* @property {string} mediaPreviewSize
*/
/**
* @typedef MediaGallery
* @type {Object}
*
* @property {ChirpBase} chirp
* @property {string} clickedMediaEntityId
*/
/**
* @typedef TwitterActionFollow
* @type {Object}
*
* @property {TwitterUser} following
*/
/**
* @typedef TwitterActionFollow_Class
* @type {Class<TwitterActionFollow>}
*/
/**
* @typedef TwitterCardJSON
* @type {Object}
*
* @property {{ [card_url]: { [string_value]: string } }} binding_values
*/
/**
* @typedef TwitterClient
* @type {Object}
*
* @property {function(chirp: ChirpBase)} callback
* @property {string} chirpId
* @property {TwitterConversations} conversations
*/
/**
* @typedef TwitterConversation
* @type {Object}
*
* @property {function} markAsRead
* @property {Array} messages
*/
/**
* @typedef TwitterConversationMessageEvent
* @extends ChirpBase
* @type {Object}
*/
/**
* @typedef TwitterConversations
* @type {Object}
*
* @property {function(id: string): TwitterConversation|null} getConversation
*/
/**
* @typedef TwitterEntity_URL
* @type {Object}
*
* @property {string} url
* @property {string} expanded_url
* @property {string} display_url
* @property {string} indices
*/
/**
* @typedef TwitterMedia
* @type {Object}
*
* @property {function: { url: string }} chooseVideoVariant
* @property {{ media_url_https: string }} entity
* @property {boolean} isAnimatedGif
* @property {boolean} isVideo
* @property {function: string} large
* @property {string} mediaId
* @property {string} service
* @property {function: string} small
*/
/**
* @typedef TwitterMedia_Class
* @type {Class<TwitterMedia>}
*
* @property {RegExp} YOUTUBE_TINY_RE
* @property {RegExp} YOUTUBE_LONG_RE
* @property {RegExp} YOUTUBE_RE
* @property {{ youtube: RegExp }} SERVICES
*/
/**
* @typedef TwitterStatus
* @type {Object}
*
* @property {TwitterCardJSON|null} card
* @property {{ urls: TwitterEntity_URL[] }} entities
*/
/**
* @typedef TwitterUser
* @type {Object}
*
* @property {string} emojifiedName
* @property {TwitterUserEntities} entities
* @property {function: string} getProfileURL
* @property {string} id
* @property {string} name
* @property {string} profileImageURL
* @property {string} screenName
* @property {string} url
*/
/**
* @typedef TwitterUserJSON
* @type {Object}
*
* @property {string} id
* @property {string} id_str
* @property {string} name
* @property {string} profile_image_url
* @property {string} profile_image_url_https
*/
/**
* @typedef TwitterUserEntities
* @type {Object}
*
* @property {{ urls: TwitterEntity_URL[] }} url
*/
/**
* @typedef TwitterUser_Account
* @type {Object}
*
* @property {function: string} getKey
*/
/** @type {TD} */
export const TD = window.TD;

View File

@ -0,0 +1,49 @@
/**
* Throws if an object is missing any property in the chain.
* @param {Object} obj
* @param {...string} chain
*/
export function ensurePropertyExists(obj, ...chain) {
for (const prop of chain) {
if (obj.hasOwnProperty(prop)) {
obj = obj[prop];
}
else {
throw "Missing property '" + prop + "' in chain [obj]." + chain.join(".");
}
}
}
/**
* Returns true if an object has every property in the chain.
* Otherwise, returns false and triggers a debug-only error message.
* @param {Object} obj
* @param {...string} chain
* @returns {boolean}
*/
export function checkPropertyExists(obj, ...chain) {
try {
ensurePropertyExists(obj, ...chain);
return true;
} catch (err) {
crashDebug(err);
return false;
}
}
/**
* Reports an error to the console, and also shows an error message if in debug mode.
*/
export function crashDebug(message) {
console.error(message);
debugger;
if ("$TD" in window) {
window.$TD.crashDebug(message);
}
}
/**
* No-op function.
*/
export function noop() {}

45
Resources/Content/bootstrap.js vendored Normal file
View File

@ -0,0 +1,45 @@
async function loadModule(path) {
let module;
try {
module = await import(path);
} catch (e) {
console.error(`[TweetDuck] Error loading '${path}': ${e}`);
return false;
}
try {
module.default();
console.info(`[TweetDuck] Successfully loaded '${path}'`);
return true;
} catch (e) {
console.error(`[TweetDuck] Error executing '${path}': ${e}`);
}
return false;
}
async function loadFeatures() {
const script = document.getElementById("tweetduck-bootstrap");
const features = script.getAttribute("data-features").split("|");
let successes = 0;
for (const feature of features) {
if (await loadModule(`./features/${feature}.js`)) {
++successes;
}
}
return [ successes, features.length ];
}
loadFeatures().then(([ successes, total ]) => {
if ("$TD" in window) {
window.$TD.onFeaturesLoaded();
}
if ("TD_PLUGINS" in window) {
window.TD_PLUGINS.onFeaturesLoaded();
}
console.info(`[TweetDuck] Successfully loaded ${successes} / ${total} feature(s).`);
});

View File

@ -0,0 +1,40 @@
import { $TD } from "../api/bridge.js";
import { onAppReady } from "../api/ready.js";
/**
* @param {HTMLUListElement} list
*/
function addMenuItems(list) {
const allDividers = list.querySelectorAll(":scope > li.drp-h-divider");
const lastDivider = allDividers[allDividers.length - 1];
lastDivider.insertAdjacentHTML("beforebegin", "<li class=\"is-selectable\" data-tweetduck><a href=\"#\" data-action>TweetDuck</a></li>");
const button = list.querySelector("[data-tweetduck]");
button.querySelector("a").addEventListener("click", function() {
$TD.openContextMenu();
});
button.addEventListener("mouseenter", function() {
button.classList.add("is-selected");
});
button.addEventListener("mouseleave", function() {
button.classList.remove("is-selected");
});
}
/**
* Adds a 'TweetDuck' menu item to the left-side Settings menu.
*/
export default function() {
onAppReady(function setupSettingsMenu() {
document.querySelector("[data-action='settings-menu']").addEventListener("click", () => setTimeout(function() {
const list = document.querySelector("nav.app-navigator .js-dropdown-content ul");
if (list) {
addMenuItems(list);
}
}, 0));
});
};

View File

@ -0,0 +1,104 @@
import { $TD } from "../api/bridge.js";
import { TD } from "../api/td.js";
import { checkPropertyExists } from "../api/utils.js";
import { replaceFunction } from "../globals/patch_functions.js";
/**
* @property {string[]} eventNames
* @property {function(this: HTMLElement, e: Event)} eventHandler
*/
function delegateLinkEvents(eventNames, eventHandler) {
for (const eventName of eventNames) {
document.body.addEventListener(eventName, function(e) {
const ele = e.target;
if (ele.hasAttribute("data-full-url")) {
eventHandler.call(ele, e);
}
}, { capture: true });
}
}
/**
* Bypasses t.co when clicking/dragging links, in media, and in user profiles.
*/
export default function() {
delegateLinkEvents([ "click", "auxclick" ], /** @this HTMLElement */ function(e) {
// event.which seems to be borked in auxclick
// tweet links open directly in the column
if ((e.button === 0 || e.button === 1) && this.getAttribute("rel") !== "tweet") {
$TD.openBrowser(this.getAttribute("data-full-url"));
e.preventDefault();
}
});
delegateLinkEvents([ "dragstart" ], /** @this HTMLElement */ function(e) {
const url = this.getAttribute("data-full-url");
const data = e.dataTransfer;
data.clearData();
data.setData("text/uri-list", url);
data.setData("text/plain", url);
data.setData("text/html", `<a href="${url}">${url}</a>`);
});
if (checkPropertyExists(TD, "services", "TwitterUser", "prototype")) {
replaceFunction(TD.services.TwitterUser.prototype, "fromJSONObject", function(func, args) {
const [ e ] = args;
const obj = func.apply(this, args);
const expandedUrl = e.entities?.url?.urls?.[0]?.expanded_url;
if (expandedUrl) {
obj.url = expandedUrl;
}
return obj;
});
}
if (checkPropertyExists(TD, "services", "TwitterMedia", "prototype")) {
replaceFunction(TD.services.TwitterMedia.prototype, "fromMediaEntity", function(func, args) {
const [ e ] = args;
const obj = func.apply(this, args);
if (e.expanded_url) {
if (obj.url === obj.shortUrl) {
obj.shortUrl = e.expanded_url;
}
obj.url = e.expanded_url;
}
return obj;
});
}
if (checkPropertyExists(TD, "services", "TwitterStatus", "prototype")) {
replaceFunction(TD.services.TwitterStatus.prototype, "_generateHTMLText", /** @this TwitterStatus */ function(func, args) {
const card = this.card;
const entities = this.entities;
if (card && entities) {
const urls = entities.urls;
if (urls && urls.length) {
const shortUrl = card.url;
const urlObj = entities.urls.find(obj => obj.url === shortUrl && obj.expanded_url);
if (urlObj) {
const expandedUrl = urlObj.expanded_url;
const innerCardUrl = card.binding_values?.card_url;
card.url = expandedUrl;
if (innerCardUrl) {
innerCardUrl.string_value = expandedUrl;
}
}
}
}
return func.apply(this, args);
});
}
};

View File

@ -0,0 +1,28 @@
import { $ } from "../api/jquery.js";
import { TD } from "../api/td.js";
import { ensurePropertyExists } from "../api/utils.js";
/**
* @typedef SearchEventData
* @type {Object}
*
* @property {string} query
* @property {string} [searchScope]
* @property {string} [columnKey]
* @property {boolean} [tduckResetInput]
*/
/**
* Clears search input after confirmation.
*/
export default function() {
ensurePropertyExists(TD, "controller", "columnManager", "_columnOrder");
ensurePropertyExists(TD, "controller", "columnManager", "move");
$(document).on("uiSearchNoTemporaryColumn", function(e, /** @type SearchEventData */ data) {
if (data.query && data.searchScope !== "users" && !data.columnKey && !("tduckResetInput" in data)) {
$(".js-app-search-input").val("");
$(".js-perform-search").blur();
}
});
};

View File

@ -0,0 +1,34 @@
import { registerPropertyUpdateCallback } from "../api/bridge.js";
import { $ } from "../api/jquery.js";
import { onAppReady } from "../api/ready.js";
import { ensurePropertyExists } from "../api/utils.js";
/**
* @typedef DateInput
* @type {Object}
*
* @property {DateInputConfiguration} conf
*/
/**
* @typedef DateInputConfiguration
* @type {Object}
*
* @property {number} firstDay
*/
/**
* Sets first day of week in date picker according to app configuration.
*/
export default function() {
ensurePropertyExists($, "tools", "dateinput", "conf", "firstDay");
/** @type DateInput */
const dateinput = $["tools"]["dateinput"];
onAppReady(function setupDatePickerFirstDayCallback() {
registerPropertyUpdateCallback(function($TDX) {
dateinput.conf.firstDay = $TDX.firstDayOfWeek;
}, true);
});
};

View File

@ -0,0 +1,16 @@
import { $TDX } from "../api/bridge.js";
import { TD } from "../api/td.js";
import { ensurePropertyExists } from "../api/utils.js";
import { replaceFunction } from "../globals/patch_functions.js";
/**
* Sets language for automatic translations.
*/
export default function() {
ensurePropertyExists(TD, "languages");
replaceFunction(TD.languages, "getSystemLanguageCode", function(func, args) {
const [ returnShortCode ] = args;
return returnShortCode ? ($TDX.translationTarget || "en") : func.apply(this, args);
});
};

View File

@ -0,0 +1,10 @@
import { $TD } from "../api/bridge.js";
/**
* Removes HTML styles when copying HTML content to clipboard.
*/
export default function() {
document.addEventListener("copy", function() {
window.setTimeout($TD.fixClipboard, 0);
});
};

View File

@ -0,0 +1,25 @@
import { getEvents } from "../api/jquery.js";
import { onAppReady } from "../api/ready.js";
import { TD } from "../api/td.js";
import { checkPropertyExists, noop } from "../api/utils.js";
/**
* Disables TweetDeck's metrics.
*/
export default function() {
TD.metrics.inflate = noop;
TD.metrics.inflateMetricTriple = noop;
TD.metrics.log = noop;
TD.metrics.makeKey = noop;
TD.metrics.send = noop;
onAppReady(function disableMetrics() {
const events = getEvents(window);
checkPropertyExists(events, "metric");
checkPropertyExists(events, "metricsFlush");
delete events["metric"];
delete events["metricsFlush"];
});
};

View File

@ -0,0 +1,80 @@
import { $ } from "../api/jquery.js";
import { TD } from "../api/td.js";
const regexTweet = /^https?:\/\/twitter\.com\/[A-Za-z0-9_]+\/status\/(\d+)\/?\??/;
const regexAccount = /^https?:\/\/twitter\.com\/(?!signup$|tos$|privacy$|search$|search-)([^/?]+)\/?$/;
/**
* Adds drag & drop behavior for dropping tweet or account links on columns to open their detail view.
*/
export default function() {
let dragType = false;
// noinspection JSUnusedGlobalSymbols
const events = {
dragover(e) {
e.originalEvent.dataTransfer.dropEffect = dragType ? "all" : "none";
e.preventDefault();
e.stopPropagation();
},
drop(e) {
const url = e.originalEvent.dataTransfer.getData("URL");
if (dragType === "tweet") {
const match = regexTweet.exec(url);
if (match.length === 2) {
const column = TD.controller.columnManager.get($(this).attr("data-column"));
if (column) {
TD.controller.clients.getPreferredClient().show(match[1], function(chirp) {
TD.ui.updates.showDetailView(column, chirp, column.findChirp(chirp.id) || chirp);
$(document).trigger("uiGridClearSelection");
}, function() {
alert("error|Could not retrieve the requested tweet.");
});
}
}
}
else if (dragType === "account") {
const match = regexAccount.exec(url);
if (match.length === 2) {
$(document).trigger("uiShowProfile", { id: match[1] });
}
}
e.preventDefault();
e.stopPropagation();
}
};
const app = $(".js-app");
const selectors = {
tweet: "section.js-column",
account: app
};
window.TDGF_onGlobalDragStart = function(type, data) {
if (dragType) {
app.off(events, selectors[dragType]);
dragType = null;
}
if (type === "link") {
if (regexTweet.test(data)) {
dragType = "tweet";
}
else if (regexAccount.test(data)) {
dragType = "account";
}
else {
dragType = null;
}
app.on(events, selectors[dragType]);
}
};
};

View File

@ -0,0 +1,66 @@
import { $TD, $TDX } from "../api/bridge.js";
/**
* @property {string} eventName
* @property {function(this: HTMLElement, e: Event)} eventHandler
*/
function delegateLinkEvent(eventName, eventHandler) {
document.body.addEventListener(eventName, function(e) {
const ele = e.target;
if (ele.hasAttribute("data-full-url")) {
eventHandler.call(ele, e);
}
}, { capture: true });
}
/**
* Either expands links or shows tooltips on hover, depending on app configuration.
*/
export default function() {
let prevMouseX = -1, prevMouseY = -1;
let tooltipTimer, tooltipDisplayed;
delegateLinkEvent("mouseenter", /** @this HTMLElement */ function() {
const text = this.innerText;
if (text.charCodeAt(text.length - 1) !== 8230 && text.charCodeAt(0) !== 8230) {
return; // horizontal ellipsis
}
if ($TDX.expandLinksOnHover) {
tooltipTimer = window.setTimeout(() => {
this.setAttribute("td-prev-text", text);
this.innerText = this.getAttribute("data-full-url").replace(/^https?:\/\/(www\.)?/, "");
}, 200);
}
else {
this.removeAttribute("title");
tooltipTimer = window.setTimeout(() => {
$TD.displayTooltip(this.getAttribute("data-full-url"));
tooltipDisplayed = true;
}, 400);
}
});
delegateLinkEvent("mouseleave", /** @this HTMLElement */ function() {
if (this.hasAttribute("td-prev-text")) {
this.innerText = this.getAttribute("td-prev-text");
}
window.clearTimeout(tooltipTimer);
if (tooltipDisplayed) {
tooltipDisplayed = false;
$TD.displayTooltip(null);
}
});
delegateLinkEvent("mousemove", /** @this HTMLElement */ function(e) {
if (tooltipDisplayed && (prevMouseX !== e.clientX || prevMouseY !== e.clientY)) {
$TD.displayTooltip(this.getAttribute("data-full-url"));
prevMouseX = e.clientX;
prevMouseY = e.clientY;
}
});
};

View File

@ -0,0 +1,21 @@
import { $TDX } from "../api/bridge.js";
import { TD } from "../api/td.js";
import { ensurePropertyExists } from "../api/utils.js";
import { runAfterFunction } from "../globals/patch_functions.js";
function focusDmInput() {
document.querySelector(".js-reply-tweetbox").focus();
}
/**
* Fixes DM reply input box not getting focused after opening a conversation.
*/
export default function() {
ensurePropertyExists(TD, "components", "ConversationDetailView", "prototype");
runAfterFunction(TD.components.ConversationDetailView.prototype, "showChirp", function() {
if ($TDX.focusDmInput) {
setTimeout(focusDmInput, 100);
}
});
};

View File

@ -0,0 +1,27 @@
import { $ } from "../api/jquery.js";
import { TD } from "../api/td.js";
import { ensurePropertyExists } from "../api/utils.js";
import { runAfterFunction } from "../globals/patch_functions.js";
import { prioritizeNewestEvent } from "../globals/prioritize_newest_event.js";
/**
* Fixes broken horizontal scrolling of column container when holding Shift.
*/
export default function() {
ensurePropertyExists(TD, "ui", "columns");
runAfterFunction(TD.ui.columns, "setupColumnScrollListeners", function(func, args) {
const [ column ] = args;
const ele = document.querySelector(".js-column[data-column='" + column.model.getKey() + "']");
if (!ele) {
return;
}
$(ele).off("onmousewheel").on("mousewheel", ".scroll-v", function(e) {
e.stopImmediatePropagation();
});
prioritizeNewestEvent(ele, "mousewheel");
});
};

View File

@ -0,0 +1,22 @@
import { $ } from "../api/jquery.js";
import { TD } from "../api/td.js";
import { ensurePropertyExists } from "../api/utils.js";
/**
* @typedef DmSentEventData
* @type {Object}
*
* @property {{ accountKey: string, conversationId: string }} request
*/
/**
* Fixes DMs not being marked as read when replying to them.
*/
export default function() {
ensurePropertyExists(TD, "controller", "clients", "getClient");
ensurePropertyExists(TD, "services", "Conversations", "prototype", "getConversation");
$(document).on("dataDmSent", function(e, /** @type DmSentEventData */ data) {
TD.controller.clients.getClient(data.request.accountKey)?.conversations.getConversation(data.request.conversationId)?.markAsRead();
});
};

View File

@ -0,0 +1,36 @@
import { TD } from "../api/td.js";
import { ensurePropertyExists } from "../api/utils.js";
import { replaceFunction } from "../globals/patch_functions.js";
const formatRegex = /\?.*format=(\w+)/;
/**
* @param {string} url
* @return {string}
*/
function fixPreviewURL(url) {
if (url.startsWith("https://ton.twitter.com/1.1/ton/data/dm/") || url.startsWith("https://pbs.twimg.com/tweet_video_thumb/")) {
const format = url.match(formatRegex);
if (format?.length === 2) {
const fix = `.${format[1]}?`;
if (!url.includes(fix)) {
return url.replace("?", fix);
}
}
}
return url;
}
/**
* Fixes DM image previews and GIF thumbnails not loading due to new URLs.
*/
export default function() {
ensurePropertyExists(TD, "services", "TwitterMedia", "prototype");
replaceFunction(TD.services.TwitterMedia.prototype, "getTwitterPreviewUrl", function(func, args) {
return fixPreviewURL(func.apply(this, args));
});
};

View File

@ -0,0 +1,23 @@
import { TD } from "../api/td.js";
import { ensurePropertyExists } from "../api/utils.js";
import { replaceFunction } from "../globals/patch_functions.js";
/**
* Adds missing languages for Bing Translator (Bengali, Icelandic, Tagalog, Tamil, Telugu, Urdu).
*/
export default function() {
ensurePropertyExists(TD, "languages", "getSupportedTranslationSourceLanguages");
const newCodes = [ "bn", "is", "tl", "ta", "te", "ur" ];
const codeSet = new Set(TD.languages.getSupportedTranslationSourceLanguages());
for (const lang of newCodes) {
codeSet.add(lang);
}
const codeList = [ ...codeSet ];
replaceFunction(TD.languages, "getSupportedTranslationSourceLanguages", function() {
return codeList;
});
};

View File

@ -0,0 +1,17 @@
import { TD } from "../api/td.js";
import { ensurePropertyExists } from "../api/utils.js";
/**
* Fixes broken OS detection.
*/
export default function() {
const doc = document.documentElement;
doc.classList.remove("os-");
doc.classList.add("os-windows");
ensurePropertyExists(TD, "util", "getOSName");
TD.util.getOSName = function() {
return "windows";
};
};

View File

@ -0,0 +1,27 @@
import { $, ensureEventExists } from "../api/jquery.js";
import { TD } from "../api/td.js";
import { ensurePropertyExists } from "../api/utils.js";
function reloadScheduledColumn() {
const column = Object.values(TD.controller.columnManager.getAll()).find(column => column.model.state.type === "scheduled");
if (column) {
setTimeout(function() {
column.reloadTweets();
}, 1000);
}
}
/**
* Works around scheduled tweets not showing up sometimes after being sent.
*/
export default function() {
ensurePropertyExists(TD, "controller", "columnManager", "getAll");
ensureEventExists(document, "dataTweetSent");
$(document).on("dataTweetSent", function(e, data) {
if ("request" in data && "scheduledDate" in data.request) {
reloadScheduledColumn();
}
});
};

View File

@ -0,0 +1,20 @@
import { TD } from "../api/td.js";
import { ensurePropertyExists } from "../api/utils.js";
/**
* Fixes youtu.be previews not showing for https links.
*/
export default function() {
ensurePropertyExists(TD, "services", "TwitterMedia");
const media = TD.services.TwitterMedia;
ensurePropertyExists(media, "YOUTUBE_TINY_RE");
ensurePropertyExists(media, "YOUTUBE_LONG_RE");
ensurePropertyExists(media, "YOUTUBE_RE");
ensurePropertyExists(media, "SERVICES", "youtube");
media.YOUTUBE_TINY_RE = new RegExp(media.YOUTUBE_TINY_RE.source.replace("http:", "https?:"));
media.YOUTUBE_RE = new RegExp(media.YOUTUBE_LONG_RE.source + "|" + media.YOUTUBE_TINY_RE.source);
media.SERVICES["youtube"] = media.YOUTUBE_RE;
};

View File

@ -0,0 +1,32 @@
import { $ } from "../api/jquery.js";
import { onAppReady } from "../api/ready.js";
import { replaceFunction } from "../globals/patch_functions.js";
import { prioritizeNewestEvent } from "../globals/prioritize_newest_event.js";
/**
* Refocuses composer input after Alt+Tab.
*/
export default function() {
onAppReady(function fixDockedComposerRefocus() {
$(document).on("tduckOldComposerActive", function() {
const ele = document.querySelector(".js-docked-compose .js-compose-text");
let cancelBlur = false;
$(ele).on("blur", function() {
cancelBlur = true;
setTimeout(function() {
cancelBlur = false;
}, 0);
});
prioritizeNewestEvent(ele, "blur");
replaceFunction(ele, "blur", function(func, args) {
if (!cancelBlur) {
func.apply(this, args);
}
});
});
});
};

View File

@ -0,0 +1,15 @@
import { $, ensureEventExists } from "../api/jquery.js";
import { onAppReady } from "../api/ready.js";
/**
* Refocuses composer input after uploading an image.
*/
export default function() {
onAppReady(function focusComposerAfterImageUpload() {
ensureEventExists(document, "uiComposeImageAdded");
$(document).on("uiComposeImageAdded", function() {
document.querySelector(".js-docked-compose .js-compose-text").focus();
});
});
};

View File

@ -0,0 +1,21 @@
import { $, $$ } from "../api/jquery.js";
import { onAppReady } from "../api/ready.js";
const refocusInput = function() {
document.querySelector(".js-docked-compose .js-compose-text").focus();
};
const accountItemClickEvent = function() {
setTimeout(refocusInput, 0);
};
/**
* Refocuses composer input after switching account.
*/
export default function() {
onAppReady(function setupAccountSwitchRefocus() {
$(document).on("tduckOldComposerActive", function() {
$$(".js-account-list", ".js-docked-compose").delegate(".js-account-item", "click", accountItemClickEvent);
});
});
};

View File

@ -0,0 +1,49 @@
import { $ } from "../api/jquery.js";
import { getHoveredColumn } from "../globals/get_hovered_column.js";
import { getHoveredTweet } from "../globals/get_hovered_tweet.js";
const tryClickSelector = function(selector, parent) {
return $(selector, parent).click().length;
};
const tryCloseModal1 = function() {
const modal = $("#open-modal");
return modal.is(":visible") && tryClickSelector("a.mdl-dismiss", modal);
};
const tryCloseModal2 = function() {
const modal = $(".js-modals-container");
return modal.length && tryClickSelector("a.mdl-dismiss", modal);
};
const tryCloseHighlightedColumn = function() {
const column = getHoveredColumn();
if (!column) {
return false;
}
const ele = $(column.ele);
return (ele.is(".is-shifted-2") && tryClickSelector(".js-tweet-social-proof-back", ele)) || (ele.is(".is-shifted-1") && tryClickSelector(".js-column-back", ele));
};
/**
* Adds support for back/forward mouse buttons.
*/
export default function() {
window.TDGF_onMouseClickExtra = function(button) {
if (button === 1) { // back button
tryClickSelector(".is-shifted-2 .js-tweet-social-proof-back", ".js-modal-panel") ||
tryClickSelector(".is-shifted-1 .js-column-back", ".js-modal-panel") ||
tryCloseModal1() ||
tryCloseModal2() ||
tryClickSelector(".js-inline-compose-close") ||
tryCloseHighlightedColumn() ||
tryClickSelector(".js-app-content.is-open .js-drawer-close:visible") ||
tryClickSelector(".is-shifted-2 .js-tweet-social-proof-back, .is-shifted-2 .js-dm-participants-back") ||
$(".is-shifted-1 .js-column-back").click();
}
else if (button === 2) { // forward button
getHoveredTweet()?.ele.children[0]?.click();
}
};
};

View File

@ -0,0 +1,48 @@
import { $TD } from "../api/bridge.js";
import { onAppReady } from "../api/ready.js";
import { TD } from "../api/td.js";
import { ensurePropertyExists } from "../api/utils.js";
import { getClassStyleProperty } from "../globals/get_class_style_property.js";
import { runAfterFunction } from "../globals/patch_functions.js";
function refreshSettings() {
const doc = document.documentElement;
const fontSizeName = TD.settings.getFontSize();
const themeName = TD.settings.getTheme();
// noinspection HtmlMissingClosingTag,HtmlRequiredLangAttribute,HtmlRequiredTitleElement
const tags = [
"<html " + Array.prototype.map.call(doc.attributes, ele => `${ele.name}="${ele.value}"`).join(" ") + "><head>"
];
for (const ele of document.head.querySelectorAll("link[rel='stylesheet']:not([data-td-exclude-notification]),meta[charset]")) {
tags.push(ele.outerHTML);
}
tags.push("<style>body { background: " + getClassStyleProperty("column-panel", "background-color") + " !important; }</style>");
doc.setAttribute("data-td-font", fontSizeName);
doc.setAttribute("data-td-theme", themeName);
$TD.loadNotificationLayout(fontSizeName, tags.join(""));
}
/**
* Hooks into TweetDeck settings object to detect when the settings change, and update html attributes and notification layout accordingly.
*/
export default function() {
ensurePropertyExists(TD, "settings", "getFontSize");
ensurePropertyExists(TD, "settings", "setFontSize");
ensurePropertyExists(TD, "settings", "getTheme");
ensurePropertyExists(TD, "settings", "setTheme");
runAfterFunction(TD.settings, "setFontSize", function() {
setTimeout(refreshSettings, 0);
});
runAfterFunction(TD.settings, "setTheme", function() {
setTimeout(refreshSettings, 0);
});
onAppReady(refreshSettings);
}

View File

@ -0,0 +1,31 @@
function createStyle(id, styles) {
const ele = document.createElement("style");
ele.id = id;
ele.innerText = styles;
document.head.appendChild(ele);
}
/**
* Adds support for injecting CSS.
*/
export default function() {
/**
* @param {string} styles
*/
window.TDGF_injectBrowserCSS = function(styles) {
if (!document.getElementById("tweetduck-browser-css")) {
createStyle("tweetduck-browser-css", styles);
}
};
/**
* @param {string|null} styles
*/
window.TDGF_reinjectCustomCSS = function(styles) {
document.getElementById("tweetduck-custom-css")?.remove();
if (styles?.length) {
createStyle("tweetduck-custom-css", styles);
}
};
};

View File

@ -0,0 +1,54 @@
import { $TDX } from "../api/bridge.js";
import { $, getEvents } from "../api/jquery.js";
/**
* Keeps the Like/Follow dialogs open if enabled in the app configuration.
*/
export default function() {
const prevSetTimeout = window.setTimeout;
const overrideState = function() {
if (!$TDX.keepLikeFollowDialogsOpen) {
return;
}
window.setTimeout = function(func, timeout) {
return timeout !== 500 && prevSetTimeout.apply(this, arguments);
};
};
const restoreState = function(context, key) {
window.setTimeout = prevSetTimeout;
if ($TDX.keepLikeFollowDialogsOpen && key in context.state) {
context.state[key] = false;
}
};
$(document).on("uiShowFavoriteFromOptions", function() {
$(".js-btn-fav", ".js-modal-inner").each(function() {
const event = getEvents(this).click[0];
const handler = event.handler;
event.handler = function() {
overrideState();
handler.apply(this, arguments);
restoreState(getEvents(document)["dataFavoriteState"][0].handler.context, "stopSubsequentLikes");
};
});
});
$(document).on("uiShowFollowFromOptions", function() {
$(".js-component", ".js-modal-inner").each(function() {
const event = getEvents(this).click[0];
const handler = event.handler;
const context = handler.context;
event.handler = function() {
overrideState();
handler.apply(this, arguments);
restoreState(context, "stopSubsequentFollows");
};
});
});
};

View File

@ -0,0 +1,22 @@
// noinspection JSUnusedGlobalSymbols
import { TD } from "../api/td.js";
import { ensurePropertyExists } from "../api/utils.js";
import { replaceFunction } from "../globals/patch_functions.js";
/**
* Limits amount of loaded DMs to avoid massive lag from re-opening them several times.
*/
export default function() {
ensurePropertyExists(TD, "services", "TwitterConversation", "prototype");
replaceFunction(TD.services.TwitterConversation.prototype, "renderThread", /** @this TwitterConversation */ function(func, args) {
const prevMessages = this.messages;
this.messages = prevMessages.slice(0, 100);
const result = func.apply(this, args);
this.messages = prevMessages;
return result;
});
};

View File

@ -0,0 +1,24 @@
import { TD } from "../api/td.js";
import { checkPropertyExists } from "../api/utils.js";
import { injectMustache } from "../globals/inject_mustache.js";
import { runAfterFunction } from "../globals/patch_functions.js";
/**
* Makes texts saying 'Retweet' lowercase.
*/
export default function() {
injectMustache("status/tweet_single.mustache", "replace", "{{_i}} Retweeted{{/i}}", "{{_i}} retweeted{{/i}}");
if (checkPropertyExists(TD, "services", "TwitterActionRetweet", "prototype")) {
runAfterFunction(TD.services.TwitterActionRetweet.prototype, "generateText", function() {
this.text = this.text.replace(" Retweeted", " retweeted");
this.htmlText = this.htmlText.replace(" Retweeted", " retweeted");
});
}
if (checkPropertyExists(TD, "services", "TwitterActionRetweetedInteraction", "prototype")) {
runAfterFunction(TD.services.TwitterActionRetweetedInteraction.prototype, "generateText", function() {
this.htmlText = this.htmlText.replace(" Retweeted", " retweeted").replace(" Retweet", " retweet");
});
}
};

View File

@ -0,0 +1,95 @@
import { $ } from "../api/jquery.js";
import { TD } from "../api/td.js";
/**
* @param {HTMLElement} ele
* @param {ChirpBase} tweet
*/
function replyInComposeDrawer(ele, tweet) {
const main = tweet.getMainTweet();
// noinspection JSUnusedGlobalSymbols
$(document).trigger("uiDockedComposeTweet", {
type: "reply",
from: [ tweet.account.getKey() ],
inReplyTo: {
id: tweet.id,
htmlText: main.htmlText,
user: {
screenName: main.user.screenName,
name: main.user.name,
profileImageURL: main.user.profileImageURL
}
},
mentions: tweet.getReplyUsers(),
element: ele
});
}
/**
* @param {ChirpBase} tweet
*/
function openFavoriteDialog(tweet) {
$(document).trigger("uiShowFavoriteFromOptions", { tweet });
}
/**
* @param {HTMLElement} ele
* @param {ChirpBase} tweet
*/
function quoteTweet(ele, tweet) {
TD.controller.stats.quoteTweet();
$(document).trigger("uiComposeTweet", {
type: "tweet",
from: [ tweet.account.getKey() ],
quotedTweet: tweet.getMainTweet(),
element: ele // triggers reply-account plugin
});
}
/**
* Adds support for middle-clicking icons under tweets for alternative behaviors:
* - Reply icon opens the compose drawer
* - Favorite icon open a 'Like from accounts...' dialog
* - Retweet icon triggers a quote
*/
export default function() {
$(".js-app").delegate(".tweet-action,.tweet-detail-action", "auxclick", function(e) {
if (e.which !== 2) {
return;
}
const column = TD.controller.columnManager.get($(this).closest("section.js-column").attr("data-column"));
if (!column) {
return;
}
const ele = $(this).closest("article");
const tweet = column.findChirp(ele.attr("data-tweet-id")) || column.findChirp(ele.attr("data-key"));
if (!tweet) {
return;
}
switch ($(this).attr("rel")) {
case "reply":
replyInComposeDrawer(ele, tweet);
break;
case "favorite":
openFavoriteDialog(tweet);
break;
case "retweet":
quoteTweet(ele, tweet);
break;
default:
return;
}
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
});
};

View File

@ -0,0 +1,16 @@
import { onAppReady } from "../api/ready.js";
/**
* Reorders search results so that accounts are above hashtags.
*/
export default function() {
onAppReady(function moveAccountsAboveHashtagsInSearch() {
const container = document.querySelector(".js-search-in-popover");
const users = container.querySelector(".js-typeahead-user-list");
const hashtags = container.querySelector(".js-typeahead-topic-list");
hashtags.insertAdjacentElement("beforebegin", users);
hashtags.classList.add("list-divider");
});
};

View File

@ -0,0 +1,63 @@
const notificationHTML = `
<div id="tweetduck-conn-issues" class="Layer NotificationListLayer">
<ul class="NotificationList">
<li class="Notification Notification--red" style="height:63px;">
<div class="Notification-inner">
<div class="Notification-icon"><span class="Icon Icon--medium Icon--circleError"></span></div>
<div class="Notification-content"><div class="Notification-body">Experiencing connection issues</div></div>
<button type="button" class="Notification-closeButton" aria-label="Close"><span class="Icon Icon--smallest Icon--close" aria-hidden="true"></span></button>
</div>
</li>
</ul>
</div>`;
function fadeOut() {
const notification = document.getElementById("tweetduck-conn-issues");
if (!notification || notification.getAnimations().some(anim => anim.id === "fade-out")) {
return;
}
const anim = notification?.animate([
{ opacity: 1 },
{ opacity: 0 }
], {
duration: 200,
id: "fade-out"
});
anim.addEventListener("finish", function() {
notification.remove();
});
}
/**
* Adds a notification that appears if the internet is disconnected.
* Currently it does not notify when the internet is connected but there is no connection to TweetDeck.
*/
export default function() {
let wasOnline = true;
const onConnectionError = function() {
if (!wasOnline) {
return;
}
wasOnline = false;
document.getElementById("tweetduck-conn-issues")?.remove();
document.body.insertAdjacentHTML("beforeend", notificationHTML);
document.querySelector("#tweetduck-conn-issues button").addEventListener("click", function() {
fadeOut(function(e) {
e.target.style.opacity = "0";
});
});
};
const onConnectionFine = function() {
wasOnline = true;
fadeOut();
};
window.addEventListener("offline", onConnectionError);
window.addEventListener("online", onConnectionFine);
};

View File

@ -0,0 +1,34 @@
import { $TD } from "../api/bridge.js";
import { $, $$ } from "../api/jquery.js";
import { onAppReady } from "../api/ready.js";
import { prioritizeNewestEvent } from "../globals/prioritize_newest_event.js";
const openSearchExternally = function(event, input) {
$TD.openBrowser("https://twitter.com/search/?q=" + encodeURIComponent(input.val() || ""));
event.preventDefault();
event.stopPropagation();
input.val("").blur();
document.querySelector(".js-app").click(); // unfocus everything
};
/**
* Submitting search queries while holding Ctrl or by middle-clicking the search icon opens the search externally.
*/
export default function() {
onAppReady(function setupExternalSearchEvents() {
$$(".js-app-search-input").on("keydown", function(e) {
(e.ctrlKey && e.keyCode === 13) && openSearchExternally(e, $(this)); // enter
});
$$(".js-perform-search").on("click auxclick", function(e) {
(e.ctrlKey || e.button === 1) && openSearchExternally(e, $(".js-app-search-input:visible"));
}).each(function() {
prioritizeNewestEvent($(this)[0], "click");
});
$$("[data-action='show-search']").on("click auxclick", function(e) {
(e.ctrlKey || e.button === 1) && openSearchExternally(e, $());
});
});
};

View File

@ -0,0 +1,36 @@
import { $TDX } from "../api/bridge.js";
import { $ } from "../api/jquery.js";
import { TD } from "../api/td.js";
import { ensurePropertyExists } from "../api/utils.js";
/**
* @typedef SearchEventData
* @type {Object}
*
* @property {string} query
* @property {string} [searchScope]
* @property {string} [columnKey]
* @property {boolean} [tduckResetInput]
*/
/**
* Creates temporary search columns at the leftmost position according to app configuration.
*/
export default function() {
ensurePropertyExists(TD, "controller", "columnManager", "_columnOrder");
ensurePropertyExists(TD, "controller", "columnManager", "move");
$(document).on("uiSearchNoTemporaryColumn", function(e, /** @type SearchEventData */ data) {
if (data.query && data.searchScope !== "users" && !data.columnKey && $TDX.openSearchInFirstColumn) {
const order = TD.controller.columnManager._columnOrder;
if (order.length > 1) {
const columnKey = order[order.length - 1];
order.splice(order.length - 1, 1);
order.splice(1, 0, columnKey);
TD.controller.columnManager.move(columnKey, "left");
}
}
});
};

View File

@ -0,0 +1,45 @@
import { $, getEvents } from "../api/jquery.js";
import { onAppReady } from "../api/ready.js";
import { ensurePropertyExists } from "../api/utils.js";
/**
* @returns {{ addFilesToUpload: function(File[]) }}
*/
function getUploader() {
const uploader = getEvents(document)["uiComposeAddImageClick"][0].handler.context;
ensurePropertyExists(Object.getPrototypeOf(uploader), "addFilesToUpload");
return uploader;
}
/**
* Allows pasting images from clipboard when editing a tweet or DM.
*/
export default function() {
onAppReady(function pasteImagesFromClipboard() {
const uploader = getUploader();
$(".js-app").delegate(".js-compose-text,.js-reply-tweetbox,.td-detect-image-paste", "paste", function(e) {
// noinspection JSValidateTypes
/** @type {{ clipboardData: DataTransfer }} */
const originalEvent = e.originalEvent;
for (const item of originalEvent.clipboardData.items) {
if (item.type.startsWith("image/")) {
const popoutDM = $(this).closest(".rpl").find(".js-reply-popout");
if (popoutDM.length) {
popoutDM.click();
}
else if ($(".js-add-image-button").is(".is-disabled")) {
// If we're already in composer and uploading additional pictures, check if the upload button is enabled,
// because the uploader object does not check for invalid state such as too many files.
return;
}
uploader.addFilesToUpload([ item.getAsFile() ]);
break;
}
}
});
});
};

View File

@ -0,0 +1,47 @@
import { getEvents } from "../api/jquery.js";
import { onAppReady } from "../api/ready.js";
import { crashDebug } from "../api/utils.js";
/**
* @typedef Searcher
* @type {Object}
*
* @property {function({ query: string, tduckResetInput: boolean })} performSearch
*/
/**
* @returns {Searcher|null}
*/
function getSearcher() {
try {
const context = getEvents(document)["uiSearchInputSubmit"][0].handler.context;
if ("performSearch" in context) {
return context;
}
} catch (e) {
crashDebug(e.toString());
}
return null;
}
/**
* @this {Searcher} searcher
* @param {string} query
*/
function performSearch(query) {
this.performSearch({ query, tduckResetInput: true });
}
export default function() {
onAppReady(function addPerformSearchFunction() {
const searcher = getSearcher();
/**
* Adds a search column with the specified query.
*/
window.TDGF_performSearch = searcher ? performSearch.bind(searcher) : function() {
alert("error|This feature is not available due to an internal error.");
};
});
};

View File

@ -0,0 +1,41 @@
import { $ } from "../api/jquery.js";
import { TD } from "../api/td.js";
import { ensurePropertyExists } from "../api/utils.js";
const pinHTML = `
<svg id="td-compose-drawer-pin" viewBox="0 0 24 24" class="icon js-show-tip" data-original-title="Stay open" data-tooltip-position="left">
<path d="M9.884,16.959l3.272,0.001l-0.82,4.568l-1.635,0l-0.817,-4.569Z"/>
<rect x="8.694" y="7.208" width="5.652" height="7.445"/>
<path d="M16.877,17.448c0,-1.908 -1.549,-3.456 -3.456,-3.456l-3.802,0c-1.907,0 -3.456,1.548 -3.456,3.456l10.714,0Z"/>
<path d="M6.572,5.676l2.182,2.183l5.532,0l2.182,-2.183l0,-1.455l-9.896,0l0,1.455Z"/>
</svg>
`;
/**
* Adds a pin icon to make tweet compose drawer stay open.
*/
export default function() {
ensurePropertyExists(TD, "settings", "getComposeStayOpen");
ensurePropertyExists(TD, "settings", "setComposeStayOpen");
$(document).on("tduckOldComposerActive", function() {
document.querySelector(".js-docked-compose .js-compose-header").insertAdjacentHTML("beforeend", pinHTML);
const pin = document.getElementById("td-compose-drawer-pin");
pin.addEventListener("click", function() {
if (TD.settings.getComposeStayOpen()) {
pin.style.transform = "rotate(0deg)";
TD.settings.setComposeStayOpen(false);
}
else {
pin.style.transform = "rotate(90deg)";
TD.settings.setComposeStayOpen(true);
}
});
if (TD.settings.getComposeStayOpen()) {
pin.style.transform = "rotate(90deg)";
}
});
};

View File

@ -0,0 +1,10 @@
import { onAppReady } from "../api/ready.js";
import { ensurePropertyExists } from "../api/utils.js";
/**
* Dispatches the 'Ready' event to all enabled plugins.
*/
export default function() {
ensurePropertyExists(window, "TD_PLUGINS");
onAppReady(() => window.TD_PLUGINS.onReady());
};

View File

@ -0,0 +1,16 @@
import { $ } from "../api/jquery.js";
/**
* Creates a `tduckOldComposerActive` event on the `document` object, which triggers when the composer is activated.
*/
export default function() {
$(document).on("uiDrawerActive uiRwebComposerOptOut", function(e, /** @type {{ activeDrawer: string }} */ data) {
if (e.type === "uiDrawerActive" && data.activeDrawer !== "compose") {
return;
}
setTimeout(function() {
$(document).trigger("tduckOldComposerActive");
}, 0);
});
};

View File

@ -0,0 +1,20 @@
import { registerPropertyUpdateCallback, triggerPropertiesUpdated } from "../api/bridge.js";
import { applyROT13 } from "../globals/apply_rot13.js";
import { getColumnName } from "../globals/get_column_name.js";
import { injectMustache } from "../globals/inject_mustache.js";
import { prioritizeNewestEvent } from "../globals/prioritize_newest_event.js";
import { reloadBrowser } from "../globals/reload_browser.js";
import { reloadColumns } from "../globals/reload_columns.js";
import { showTweetDetail } from "../globals/show_tweet_detail.js";
export default function() {
window.TDGF_applyROT13 = applyROT13;
window.TDGF_getColumnName = getColumnName;
window.TDGF_injectMustache = injectMustache;
window.TDGF_onPropertiesUpdated = triggerPropertiesUpdated;
window.TDGF_prioritizeNewestEvent = prioritizeNewestEvent;
window.TDGF_registerPropertyUpdateCallback = registerPropertyUpdateCallback;
window.TDGF_reload = reloadBrowser;
window.TDGF_reloadColumns = reloadColumns;
window.TDGF_showTweetDetail = showTweetDetail;
};

View File

@ -0,0 +1,46 @@
import { $ } from "../api/jquery.js";
import { TD } from "../api/td.js";
import { checkPropertyExists } from "../api/utils.js";
import { replaceFunction } from "../globals/patch_functions.js";
/**
* Allows restoring cleared columns by holding Shift.
*/
export default function() {
let holdingShift = false;
const updateShiftState = (pressed) => {
if (holdingShift !== pressed) {
holdingShift = pressed;
$("button[data-action='clear']").children("span").text(holdingShift ? "Restore" : "Clear");
}
};
document.addEventListener("keydown", function(e) {
if (e.shiftKey && (document.activeElement === null || !("value" in document.activeElement))) {
updateShiftState(true);
}
});
document.addEventListener("keyup", function(e) {
if (!e.shiftKey) {
updateShiftState(false);
}
});
if (checkPropertyExists(TD, "vo", "Column", "prototype")) {
replaceFunction(TD.vo.Column.prototype, "clear", function(func, args) {
window.setTimeout(function() {
document.activeElement.blur(); // unfocuses the Clear button, otherwise it steals keyboard input
}, 0);
if (holdingShift) {
this.model.setClearedTimestamp(0);
this.reloadTweets();
}
else {
func.apply(this, args);
}
});
}
};

View File

@ -0,0 +1,65 @@
import { $TD } from "../api/bridge.js";
import { $ } from "../api/jquery.js";
import { getClassStyleProperty } from "../globals/get_class_style_property.js";
import { getHoveredTweet } from "../globals/get_hovered_tweet.js";
export default function() {
/**
* Screenshots the hovered tweet to clipboard.
*/
window.TDGF_triggerScreenshot = function() {
const hovered = getHoveredTweet();
if (!hovered) {
return;
}
const columnWidth = $(hovered.column.ele).width();
const tweet = hovered.wrap || hovered.obj;
const html = $(tweet.render({
withFooter: false,
withTweetActions: false,
isInConvo: false,
isFavorite: false,
isRetweeted: false, // keeps retweet mark above tweet
isPossiblySensitive: false,
mediaPreviewSize: hovered.column.obj.getMediaPreviewSize()
}));
html.find("footer").last().remove(); // apparently withTweetActions breaks for certain tweets, nice
html.find(".td-screenshot-remove").remove();
html.find("p.link-complex-target,p.txt-mute").filter(function() {
return $(this).text() === "Show this thread";
}).remove();
html.addClass($(document.documentElement).attr("class"));
html.addClass($(document.body).attr("class"));
html.css("background-color", getClassStyleProperty("stream-item", "background-color"));
html.css("border", "none");
for (const selector of [ ".js-quote-detail", ".js-media-preview-container", ".js-media" ]) {
const ele = html.find(selector);
if (ele.length) {
ele[0].style.setProperty("margin-bottom", "2px", "important");
break;
}
}
const gif = html.find(".js-media-gif-container");
if (gif.length) {
gif.css("background-image", "url(\"" + tweet.getMedia()[0].small() + "\")");
}
const type = tweet.getChirpType();
if ((type.startsWith("favorite") || type.startsWith("retweet")) && tweet.isAboutYou()) {
html.addClass("td-notification-padded");
}
$TD.screenshotTweet(html[0].outerHTML, columnWidth);
};
};

View File

@ -0,0 +1,25 @@
import { $, single } from "../api/jquery.js";
/**
* @typedef TD_Event_uiColumnRendered_data
* @type {Object}
*
* @property {TD_Column} column
* @property {JQuery<HTMLElement>} $column
*/
/**
* Adds column icons as attributes on the column element to column types stylable.
*/
export default function() {
$(document).on("uiColumnRendered", function(e, data) {
const ele = single(data.$column);
const icon = ele?.querySelector(".column-type-icon");
const name = icon && Array.prototype.find.call(icon.classList, cls => cls.startsWith("icon-"));
if (name) {
ele.setAttribute("data-td-icon", name);
data.column._tduck_icon = name;
}
});
}

View File

@ -0,0 +1,243 @@
import { $TD, $TDX } from "../api/bridge.js";
import { $ } from "../api/jquery.js";
import { TD } from "../api/td.js";
import { checkPropertyExists, ensurePropertyExists } from "../api/utils.js";
import { getColumnName } from "../globals/get_column_name.js";
import { replaceFunction } from "../globals/patch_functions.js";
/**
* Event callback for a new tweet.
* @returns {function(column: TD_Column, tweet: ChirpBase)}
*/
const onNewTweet = (function() {
const recentMessages = new Set();
const recentTweets = new Set();
let recentTweetTimer = null;
const resetRecentTweets = () => {
recentTweetTimer = null;
recentTweets.clear();
};
const startRecentTweetTimer = () => {
if (recentTweetTimer) {
window.clearTimeout(recentTweetTimer);
}
recentTweetTimer = window.setTimeout(resetRecentTweets, 20000);
};
const checkTweetCache = (set, id) => {
if (set.has(id)) {
return true;
}
if (set.size > 50) {
set.clear();
}
set.add(id);
return false;
};
const isSensitive = (tweet) => {
const main = tweet.getMainTweet && tweet.getMainTweet();
if (main?.possiblySensitive) {
return true; // TODO these don't show media badges when hiding sensitive media
}
const related = tweet.getRelatedTweet && tweet.getRelatedTweet();
if (related?.possiblySensitive) {
return true;
}
// noinspection RedundantIfStatementJS
if (tweet.quotedTweet?.possiblySensitive) {
return true;
}
return false;
};
const fixMedia = function(html, media) {
return html.find("a[data-media-entity-id='" + media.mediaId + "'], .media-item").first().removeClass("is-zoomable").css("background-image", "url(\"" + media.small() + "\")");
};
/**
* @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 (column.model.getHasNotification()) {
const sensitive = isSensitive(tweet);
const previews = $TDX.notificationMediaPreviews && (!sensitive || TD.settings.getDisplaySensitiveMedia());
// TODO new cards don't have either previews or links
const html = $(tweet.render({
withFooter: false,
withTweetActions: false,
withMediaPreview: true,
isMediaPreviewOff: !previews,
isMediaPreviewSmall: previews,
isMediaPreviewLarge: false,
isMediaPreviewCompact: false,
isMediaPreviewInQuoted: previews,
thumbSizeClass: "media-size-medium",
mediaPreviewSize: "medium"
}));
html.find("footer").last().remove(); // apparently withTweetActions breaks for certain tweets, nice
html.find(".js-quote-detail").removeClass("is-actionable margin-b--8"); // prevent quoted tweets from changing the cursor and reduce bottom margin
if (previews) {
html.find(".reverse-image-search").remove();
const container = html.find(".js-media");
for (const media of tweet.getMedia()) {
fixMedia(container, media);
}
if (tweet.quotedTweet) {
for (const media of tweet.quotedTweet.getMedia()) {
fixMedia(container, media).addClass("media-size-medium");
}
}
}
else if (tweet instanceof TD.services.TwitterActionOnTweet) {
html.find(".js-media").remove();
}
html.find("a[data-full-url]").each(function() { // bypass t.co on all links and fix tooltips
this.href = this.getAttribute("data-full-url");
this.removeAttribute("title");
});
html.find("a[href='#']").each(function() { // remove <a> tags around links that don't lead anywhere (such as account names the tweet replied to)
this.outerHTML = this.innerHTML;
});
html.find("p.link-complex-target").filter(function() {
return $(this).text() === "Show this thread";
}).first().each(function() {
this.id = "tduck-show-thread";
const moveBefore = html.find(".tweet-body > .js-media, .tweet-body > .js-media-preview-container, .quoted-tweet");
if (moveBefore) {
$(this).css("margin-top", "5px").removeClass("margin-b--5").parent("span").detach().insertBefore(moveBefore);
}
});
if (tweet.quotedTweet) {
html.find("p.txt-mute").filter(function() {
return $(this).text() === "Show this thread";
}).first().remove();
}
const type = tweet.getChirpType();
if (type === "follow") {
html.find(".js-user-actions-menu").parent().remove();
html.find(".account-bio").removeClass("padding-t--5").css("padding-top", "2px");
}
else if ((type.startsWith("favorite") || type.startsWith("retweet")) && tweet.isAboutYou()) {
html.children().first().addClass("td-notification-padded");
}
else if (type.includes("list_member")) {
html.children().first().addClass("td-notification-padded td-notification-padded-alt");
html.find(".activity-header").css("margin-top", "2px");
html.find(".avatar").first().css("margin-bottom", "0");
}
if (sensitive) {
html.find(".media-badge").each(function() {
$(this)[0].lastChild.textContent += " (possibly sensitive)";
});
}
const source = tweet.getRelatedTweet();
const duration = source ? (source.text.length + (source.quotedTweet?.text.length ?? 0)) : tweet.text.length;
const chirpId = source ? source.id : "";
const tweetUrl = source ? source.getChirpURL() : "";
const quoteUrl = source && source.quotedTweet ? source.quotedTweet.getChirpURL() : "";
$TD.onTweetPopup(column.model.privateState.apiid, chirpId, getColumnName(column), html.html(), duration, tweetUrl, quoteUrl);
}
if (column.model.getHasSound()) {
$TD.onTweetSound();
}
};
})();
/**
* Fixes DM notifications not showing if the conversation is open.
* @this {TD_Column}
* @param {{ chirps: ChirpBase[], poller: { feed: { managed: boolean } } }} e
*/
function handleNotificationEvent(e) {
if (this.model?.state?.type === "privateMe" && !this.notificationsDisabled && e.poller.feed.managed) {
const unread = [];
for (const chirp of e.chirps) {
if (Array.isArray(chirp.messages)) {
Array.prototype.push.apply(unread, chirp.messages.filter(message => message.read === false));
}
}
if (unread.length > 0) {
if (checkPropertyExists(TD, "util", "chirpReverseColumnSort")) {
unread.sort(TD.util.chirpReverseColumnSort);
}
for (const message of unread) {
onNewTweet(this, message);
}
// TODO sound notifications are borked as well
// TODO figure out what to do with missed notifications at startup
}
}
}
/**
* Adds support for desktop notifications.
*/
export default function() {
ensurePropertyExists(TD, "controller", "notifications");
TD.controller.notifications.hasNotifications = function() {
return true;
};
TD.controller.notifications.isPermissionGranted = function() {
return true;
};
$["subscribe"]("/notifications/new", function(obj) {
for (let index = obj.items.length - 1; index >= 0; index--) {
onNewTweet(obj.column, obj.items[index]);
}
});
if (checkPropertyExists(TD, "vo", "Column", "prototype")) {
replaceFunction(TD.vo.Column.prototype, "mergeMissingChirps", function(func, args) {
handleNotificationEvent.call(this, args[0]);
return func.apply(this, args);
});
}
};

View File

@ -0,0 +1,42 @@
import { $TD } from "../api/bridge.js";
import { getHoveredTweet } from "../globals/get_hovered_tweet.js";
/**
* @this HTMLAnchorElement
*/
function handleLinkContextMenu() {
if (this.classList.contains("js-media-image-link")) {
const hovered = getHoveredTweet();
if (!hovered) {
return;
}
const tweet = hovered.obj.hasMedia() ? hovered.obj : hovered.obj.quotedTweet;
const media = tweet.getMedia().find(media => media.mediaId === this.getAttribute("data-media-entity-id"));
if ((media.isVideo && media.service === "twitter") || media.isAnimatedGif) {
$TD.setRightClickedLink("video", media.chooseVideoVariant().url);
}
else {
$TD.setRightClickedLink("image", media.large());
}
}
else if (this.classList.contains("js-gif-play")) {
$TD.setRightClickedLink("video", this.closest(".js-media-gif-container")?.querySelector("video")?.src);
}
else if (this.hasAttribute("data-full-url")) {
$TD.setRightClickedLink("link", this.getAttribute("data-full-url"));
}
}
/**
* Adds additional information about links to the context menu.
*/
export default function() {
document.body.addEventListener("contextmenu", function(e) {
const ele = e.target;
if (ele.tagName === "A") {
handleLinkContextMenu.call(ele);
}
}, { capture: true });
};

View File

@ -0,0 +1,42 @@
import { $TDX } from "../api/bridge.js";
import { replaceFunction } from "../globals/patch_functions.js";
/**
* Adds support for sound notification settings.
*/
export default function() {
/**
* @param {boolean} isCustom
* @param {number} volume
*/
window.TDGF_setSoundNotificationData = function(isCustom, volume) {
/** @type {HTMLAudioElement} */
const audio = document.getElementById("update-sound");
audio.volume = volume / 100;
const customSourceId = "tduck-custom-sound-source";
const existingCustomSource = document.getElementById(customSourceId);
if (isCustom && !existingCustomSource) {
const newCustomSource = document.createElement("source");
newCustomSource.id = customSourceId;
newCustomSource.src = "https://ton.twimg.com/tduck/updatesnd";
audio.prepend(newCustomSource);
}
else if (!isCustom && existingCustomSource) {
audio.removeChild(existingCustomSource);
}
audio.load();
};
window.TDGF_playSoundNotification = function() {
document.getElementById("update-sound").play();
};
replaceFunction(HTMLAudioElement.prototype, "play", function(func, args) {
if (!$TDX.muteNotifications) {
func.apply(this, args);
}
});
};

View File

@ -0,0 +1,46 @@
import { $TD } from "../api/bridge.js";
import { TD } from "../api/td.js";
import { ensurePropertyExists } from "../api/utils.js";
import { getHoveredTweet } from "../globals/get_hovered_tweet.js";
function processMedia(chirp) {
return chirp.getMedia().filter(item => !item.isAnimatedGif).map(item => item.entity.media_url_https + ":small").join(";");
}
function handleTweetContextMenu() {
const hovered = getHoveredTweet();
if (!hovered) {
return;
}
const tweet = hovered.obj;
const quote = tweet.quotedTweet;
if (tweet.chirpType === TD.services.ChirpBase.TWEET) {
const tweetUrl = tweet.getChirpURL();
const quoteUrl = quote && quote.getChirpURL();
const chirpAuthors = quote ? [ tweet.getMainUser().screenName, quote.getMainUser().screenName ].join(";") : tweet.getMainUser().screenName;
const chirpImages = tweet.hasImage() ? processMedia(tweet) : quote?.hasImage() ? processMedia(quote) : "";
$TD.setRightClickedChirp(tweetUrl || "", quoteUrl || "", chirpAuthors, chirpImages);
}
else if (tweet instanceof TD.services.TwitterActionFollow) {
$TD.setRightClickedLink("link", tweet.following.getProfileURL());
}
}
/**
* Adds additional information about tweets to the context menu.
*/
export default function() {
ensurePropertyExists(TD, "controller", "columnManager", "get");
ensurePropertyExists(TD, "services", "ChirpBase", "TWEET");
ensurePropertyExists(TD, "services", "TwitterActionFollow");
document.querySelector(".js-app").addEventListener("contextmenu", function(e) {
if (e.target.closest("section.js-column") !== null) {
handleTweetContextMenu();
}
}, { capture: true });
};

View File

@ -0,0 +1,60 @@
import { TD } from "../api/td.js";
import { checkPropertyExists } from "../api/utils.js";
import { replaceFunction } from "../globals/patch_functions.js";
/**
* Replaces displayed name and avatar of the official TweetDuck account.
*/
export default function() {
const realDisplayName = "TweetDuck";
const realAvatar = "https://ton.twimg.com/tduck/avatar";
const accountId = "957608948189880320";
if (checkPropertyExists(TD, "services", "TwitterUser", "prototype")) {
replaceFunction(TD.services.TwitterUser.prototype, "fromJSONObject", function(func, args) {
/** @type TwitterUser */
const user = func.apply(this, args);
if (user.id === accountId) {
user.name = realDisplayName;
user.emojifiedName = realDisplayName;
user.profileImageURL = realAvatar;
user.url = "https://tweetduck.chylex.com";
if (user.entities && user.entities.url) {
user.entities.url.urls = [{
url: user.url,
expanded_url: user.url,
display_url: "tweetduck.chylex.com",
indices: [ 0, 23 ]
}];
}
}
return user;
});
}
if (checkPropertyExists(TD, "services", "TwitterClient", "prototype")) {
replaceFunction(TD.services.TwitterClient.prototype, "typeaheadSearch", function(func, args) {
const [ data, onSuccess, onError ] = args;
if (data.query?.toLowerCase().endsWith("tweetduck")) {
data.query = "TryMyAwesomeApp";
}
return func.call(this, data, function(/** @type {{ users: TwitterUserJSON[] }} */ result) {
for (const user of result.users) {
if (user.id_str === accountId) {
user.name = realDisplayName;
user.profile_image_url = realAvatar;
user.profile_image_url_https = realAvatar;
break;
}
}
onSuccess.apply(this, arguments);
}, onError);
});
}
};

View File

@ -0,0 +1,147 @@
import { $TD } from "../api/bridge.js";
import { TD } from "../api/td.js";
import { checkPropertyExists, noop } from "../api/utils.js";
import { getHoveredTweet } from "../globals/get_hovered_tweet.js";
import { injectMustache } from "../globals/inject_mustache.js";
import { replaceFunction, runAfterFunction } from "../globals/patch_functions.js";
/**
* @param {HTMLElement} ele
* @returns {string|null}
*/
function getGifLink(ele) {
if (!ele) {
return null;
}
return (ele.getAttribute("src") || ele.querySelector("source[video-src]")?.getAttribute("video-src"));
}
/**
* @param {HTMLElement} obj
* @returns {string|null}
*/
function getVideoTweetLink(obj) {
const parent = obj.querySelector(".js-tweet");
if (!parent) {
return null;
}
const link = parent.classList.contains("tweet-detail") ? parent.querySelector("a[rel='url']") : parent.querySelector("time > a");
return link?.getAttribute("href");
}
/**
* @param {ChirpBase|null} tweet
* @returns {string|null}
*/
function getUsername(tweet) {
return tweet && (tweet.quotedTweet || tweet).getMainUser().screenName;
}
/**
* @param {string} selector
* @param {function(this: HTMLElement, e: MouseEvent)} handler
* @returns {function(e: MouseEvent)}
*/
function delegateMouseEvent(selector, handler) {
return function(e) {
const ele = e.target.closest(selector);
if (ele) {
handler.call(ele, e);
}
};
}
function createOverlay() {
const stopVideo = function() {
$TD.stopVideo();
};
const overlay = document.createElement("div");
overlay.id = "td-video-player-overlay";
overlay.classList.add("ovl");
overlay.style.display = "block";
overlay.addEventListener("click", stopVideo);
overlay.addEventListener("contextmenu", stopVideo);
document.querySelector(".js-app").appendChild(overlay);
}
export default function() {
const app = document.querySelector(".js-app");
/**
* Plays a video using the internal player.
* @param {string} videoUrl
* @param {string|null} tweetUrl
* @param {string|null} username
*/
window.TDGF_playVideo = function(videoUrl, tweetUrl, username) {
if (!videoUrl) {
return;
}
$TD.playVideo(videoUrl, tweetUrl || videoUrl, username || null, createOverlay);
};
app.addEventListener("click", delegateMouseEvent(".js-gif-play", function(e) {
const src = !e.ctrlKey && getGifLink(this.closest(".js-media-gif-container")?.querySelector("video"));
const tweet = getVideoTweetLink(this);
if (src) {
const hovered = getHoveredTweet();
window.TDGF_playVideo(src, tweet, getUsername(hovered && hovered.obj));
}
else {
$TD.openBrowser(tweet);
}
e.stopPropagation();
}));
app.addEventListener("mousedown", delegateMouseEvent(".js-gif-player", function(e) {
if (e.button === 1) {
e.preventDefault();
}
}));
app.addEventListener("mouseup", delegateMouseEvent(".js-gif-player", function(e) {
if (e.button === 1) {
$TD.openBrowser(getVideoTweetLink(this));
e.preventDefault();
}
}));
injectMustache("status/media_thumb.mustache", "append", "is-gif", " is-paused");
// noinspection HtmlUnknownTarget, CssUnknownTarget
TD.mustaches["media/native_video.mustache"] = "<div class=\"js-media-gif-container media-item nbfc is-video\" style=\"background-image:url({{imageSrc}});\"><video class=\"js-media-gif media-item-gif full-width block {{#isPossiblySensitive}}is-invisible{{/isPossiblySensitive}}\" loop src=\"{{videoUrl}}\"></video><a class=\"js-gif-play pin-all is-actionable\">{{> media/video_overlay}}</a></div>";
let cancelModal = false;
if (checkPropertyExists(TD, "components", "MediaGallery", "prototype")) {
runAfterFunction(TD.components.MediaGallery.prototype, "_loadTweet", /** @this MediaGallery */ function() {
const media = this.chirp.getMedia().find(media => media.mediaId === this.clickedMediaEntityId);
if (media && media.isVideo && media.service === "twitter") {
window.TDGF_playVideo(media.chooseVideoVariant().url, this.chirp.getChirpURL(), getUsername(this.chirp));
cancelModal = true;
}
});
}
if (checkPropertyExists(TD, "components", "BaseModal", "prototype")) {
replaceFunction(TD.components.BaseModal.prototype, "setAndShowContainer", function(func, args) {
if (cancelModal) {
cancelModal = false;
}
else {
func.apply(this, args);
}
});
}
if (checkPropertyExists(TD, "ui", "Column", "prototype", "playGifIfNotManuallyPaused")) {
TD.ui.Column.prototype.playGifIfNotManuallyPaused = noop;
}
};

View File

@ -0,0 +1,14 @@
import { TD } from "../api/td.js";
import { ensurePropertyExists } from "../api/utils.js";
import { replaceFunction } from "../globals/patch_functions.js";
/**
* Skips the pre-login page so that users immediately see the login page.
*/
export default function() {
ensurePropertyExists(TD, "controller", "init");
replaceFunction(TD.controller.init, "showLogin", function() {
location.href = "https://twitter.com/login?hide_message=true&redirect_after_login=https%3A%2F%2Ftweetdeck.twitter.com%2F%3Fvia_twitter_login%3Dtrue";
});
};

View File

@ -0,0 +1,24 @@
// noinspection FunctionNamingConventionJS
/**
* Applies the ROT13 cipher to the selected input text.
*/
export function applyROT13() {
const activeElement = document.activeElement;
const inputValue = activeElement?.value;
if (!inputValue) {
return;
}
const selection = inputValue.substring(activeElement.selectionStart, activeElement.selectionEnd);
if (!selection) {
return;
}
// noinspection JSDeprecatedSymbols
document.execCommand("insertText", false, selection.replace(/[a-zA-Z]/g, function(chr) {
const code = chr.charCodeAt(0);
const start = code <= 90 ? 65 : 97;
return String.fromCharCode(start + (code - start + 13) % 26);
}));
}

View File

@ -0,0 +1,17 @@
/**
* Retrieves the actual value of a CSS property of an element with the specified class.
* @param {string} elementClass
* @param {string} cssProperty
* @returns {string}
*/
export function getClassStyleProperty(elementClass, cssProperty) {
const column = document.createElement("div");
column.classList.add(elementClass);
column.style.display = "none";
document.body.appendChild(column);
const value = window.getComputedStyle(column).getPropertyValue(cssProperty);
document.body.removeChild(column);
return value;
}

View File

@ -0,0 +1,25 @@
const columnIconsToNames = {
"icon-home": "Home",
"icon-mention": "Mentions",
"icon-message": "Messages",
"icon-notifications": "Notifications",
"icon-follow": "Followers",
"icon-activity": "Activity",
"icon-favorite": "Likes",
"icon-user": "User",
"icon-search": "Search",
"icon-list": "List",
"icon-custom-timeline": "Timeline",
"icon-dataminr": "Dataminr",
"icon-play-video": "Live video",
"icon-schedule": "Scheduled"
};
/**
* Returns the display name of a column, or an empty string if the column type is unknown.
* @param {TD_Column} column
* @returns {string}
*/
export function getColumnName(column) {
return columnIconsToNames[column._tduck_icon] || "";
}

View File

@ -0,0 +1,23 @@
import { TD } from "../api/td.js";
/**
* Returns an object containing data about the column below the cursor.
* @returns {{ ele: Element, obj: TD_Column }|null}
*/
export function getHoveredColumn() {
const hovered = document.querySelectorAll(":hover");
for (let index = hovered.length - 1; index >= 0; index--) {
const ele = hovered[index];
if (ele.tagName === "SECTION" && ele.classList.contains("js-column")) {
const obj = TD.controller.columnManager.get(ele.getAttribute("data-column"));
if (obj) {
return { ele, obj };
}
}
}
return null;
}

View File

@ -0,0 +1,28 @@
import { getHoveredColumn } from "./get_hovered_column.js";
/**
* Returns an object containing data about the tweet below the cursor.
* @returns {{ ele: Element, obj: ChirpBase, wrap: ChirpBase, column: { ele: Element, obj: TD_Column }}|null}
*/
export function getHoveredTweet() {
const hovered = document.querySelectorAll(":hover");
for (let index = hovered.length - 1; index >= 0; index--) {
const ele = hovered[index];
if (ele.tagName === "ARTICLE" && ele.classList.contains("js-stream-item") && ele.hasAttribute("data-account-key")) {
const column = getHoveredColumn();
if (column) {
const wrap = column.obj.findChirp(ele.getAttribute("data-key"));
const obj = column.obj.findChirp(ele.getAttribute("data-tweet-id")) || wrap;
if (obj) {
return { ele, obj, wrap, column };
}
}
}
}
return null;
}

View File

@ -0,0 +1,47 @@
import { TD } from "../api/td.js";
import { crashDebug } from "../api/utils.js";
/**
* Injects custom HTML into mustache templates.
* @param {string} name
* @param {"replace"|"append"|"prepend"} operation
* @param {string} search
* @param {string} custom
* @returns {boolean}
*/
export function injectMustache(name, operation, search, custom) {
let replacement;
switch (operation) {
case "replace":
replacement = custom;
break;
case "append":
replacement = search + custom;
break;
case "prepend":
replacement = custom + search;
break;
default:
throw "Invalid mustache injection operation. Only 'replace', 'append', 'prepend' are supported.";
}
const prev = TD.mustaches?.[name];
if (!prev) {
crashDebug("Mustache injection is referencing an invalid mustache: " + name);
return false;
}
TD.mustaches[name] = prev.replace(search, replacement);
if (prev === TD.mustaches[name]) {
crashDebug("Mustache injection had no effect: " + name);
return false;
}
return true;
}

View File

@ -0,0 +1,48 @@
import { crashDebug } from "../api/utils.js";
/**
* @callback FunctionReplacementCallback
* @param {function} func original function
* @param {*[]} args original arguments
* @returns {*} the value to return from the replaced function
*/
/**
* Replaces a function `ownerObject.functionName` with a callback.
* The callback is bound to the original function's `this`.
* @param {Object} ownerObject
* @param {string} functionName
* @param {FunctionReplacementCallback} callback
* @returns {boolean} whether the replacement was successful
*/
export function replaceFunction(ownerObject, functionName, callback) {
const originalFunction = ownerObject[functionName];
if (typeof originalFunction !== "function") {
crashDebug("Missing function '" + functionName + "' for replacement!");
return false;
}
ownerObject[functionName] = function() {
return callback.call(this, originalFunction, arguments);
};
return true;
}
/**
* Replaces a function `ownerObject.functionName` with one that calls the provided callback after the original function returns.
* The callback is bound to the original function's `this`.
* Anything the callback returns is ignored.
* @param {Object} ownerObject
* @param {string} functionName
* @param {function} callback
* @returns {boolean} whether the replacement was successful
*/
export function runAfterFunction(ownerObject, functionName, callback) {
return replaceFunction(ownerObject, functionName, function(func, args) {
const result = func.apply(this, args);
callback.call(this, func, args);
return result;
});
}

View File

@ -0,0 +1,19 @@
import { getEvents } from "../api/jquery.js";
/**
* Pushes the newest jQuery event to the beginning of the event handler list, so that it runs before anything else.
* @param {EventTarget} element
* @param {string} eventName
*/
export function prioritizeNewestEvent(element, eventName) {
const events = getEvents(element);
const handlers = events[eventName];
const newHandler = handlers[handlers.length - 1];
for (let index = handlers.length - 1; index > 0; index--) {
handlers[index] = handlers[index - 1];
}
handlers[0] = newHandler;
}

View File

@ -0,0 +1,17 @@
let isReloading = false;
/**
* Reloads the website with memory cleanup if available.
*/
export function reloadBrowser() {
if (isReloading) {
return;
}
if ("gc" in window) {
window.gc();
}
isReloading = true;
window.location.reload();
}

View File

@ -0,0 +1,12 @@
import { TD } from "../api/td.js";
import { checkPropertyExists, noop } from "../api/utils.js";
function isSupported() {
return checkPropertyExists(TD, "controller", "columnManager", "getAll");
}
function reloadColumnsImpl() {
Object.values(TD.controller.columnManager.getAll()).forEach(column => column.reloadTweets());
}
export const reloadColumns = isSupported() ? reloadColumnsImpl : noop;

View File

@ -0,0 +1,68 @@
import { $TD } from "../api/bridge.js";
import { $ } from "../api/jquery.js";
import { isAppReady, onAppReady } from "../api/ready.js";
import { TD } from "../api/td.js";
import { checkPropertyExists } from "../api/utils.js";
function isSupported() {
return checkPropertyExists(TD, "ui", "updates", "showDetailView") &&
checkPropertyExists(TD, "controller", "columnManager", "showColumn") &&
checkPropertyExists(TD, "controller", "columnManager", "getByApiid") &&
checkPropertyExists(TD, "controller", "clients", "getPreferredClient");
}
/**
* @param {TD_Column} column
* @param {ChirpBase} chirp
*/
function showTweetDetailInternal(column, chirp) {
TD.ui.updates.showDetailView(column, chirp, column.findChirp(chirp.id) || chirp);
TD.controller.columnManager.showColumn(column.model.privateState.key);
$(document).trigger("uiGridClearSelection");
}
/**
* @param {string} columnId
* @param {string} chirpId
* @param {string} fallbackUrl
*/
function showTweetDetailImpl(columnId, chirpId, fallbackUrl) {
if (!isAppReady()) {
onAppReady(() => showTweetDetailImpl(columnId, chirpId, fallbackUrl));
return;
}
const column = TD.controller.columnManager.getByApiid(columnId);
if (!column) {
if (confirm("error|The column which contained the tweet no longer exists. Would you like to open the tweet in your browser instead?")) {
$TD.openBrowser(fallbackUrl);
}
return;
}
const chirp = column.findMostInterestingChirp(chirpId);
if (chirp) {
showTweetDetailInternal(column, chirp);
}
else {
TD.controller.clients.getPreferredClient().show(chirpId, function(chirp) {
showTweetDetailInternal(column, chirp);
}, function() {
if (confirm("error|Could not retrieve the requested tweet. Would you like to open the tweet in your browser instead?")) {
$TD.openBrowser(fallbackUrl);
}
});
}
}
/**
* Opens the tweet detail view in the specified column.
* @param {string} columnId
* @param {string} chirpId
* @param {string} fallbackUrl
*/
export const showTweetDetail = isSupported() ? showTweetDetailImpl : function() {
alert("error|This feature is not available due to an internal error.");
};

View File

@ -79,7 +79,7 @@ enabled(){
else{
$(document).one("dataSettingsValues", () => this.onStageReady());
}
$TDP.checkFileExists(this.$token, configFile).then(exists => {
if (!exists){
loadConfigObject(null);
@ -129,7 +129,7 @@ enabled(){
}, function(){
$(this).removeClass("is-selected");
});
}, 1);
}, 2);
};
// modal dialog setup

View File

@ -0,0 +1,62 @@
(function() {
const features = [
"add_tweetduck_to_settings_menu",
"bypass_t.co_links",
"clear_search_input",
"configure_first_day_of_week",
"configure_language_for_translations",
"disable_clipboard_formatting",
"disable_td_metrics",
"drag_links_onto_columns",
"expand_links_or_show_tooltip",
"fix_dm_input_box_focus",
"fix_horizontal_scrolling_of_column_container",
"fix_marking_dm_as_read_when_replying",
"fix_media_preview_urls",
"fix_missing_bing_translator_languages",
"fix_os_name",
"fix_scheduled_tweets_not_appearing",
"fix_youtube_previews",
"focus_composer_after_alt_tab",
"focus_composer_after_image_upload",
"focus_composer_after_switching_account",
"handle_extra_mouse_buttons",
"hook_theme_settings",
"inject_css",
"keep_like_follow_dialogs_open",
"limit_loaded_dm_count",
"make_retweets_lowercase",
"middle_click_tweet_icon_actions",
"move_accounts_above_hashtags_in_search",
"offline_notification",
"open_search_externally",
"open_search_in_first_column",
"paste_images_from_clipboard",
"perform_search",
"pin_composer_icon",
"ready_plugins",
"register_composer_active_event",
"register_global_functions",
"restore_cleared_column",
"screenshot_tweet",
"setup_column_type_attributes",
"setup_desktop_notifications",
"setup_link_context_menu",
"setup_sound_notifications",
"setup_tweet_context_menu",
"setup_tweetduck_account_bamboozle",
"setup_video_player",
"skip_pre_login_page",
];
document.documentElement.id = "tduck";
window.jQuery = window.$;
const script = document.createElement("script");
script.id = "tweetduck-bootstrap";
script.type = "text/javascript";
script.async = false;
script.src = "td://resources/bootstrap.js";
script.setAttribute("data-features", features.join("|"));
document.head.appendChild(script);
})();

View File

@ -1,465 +0,0 @@
(function($, TD){
if ($ === null){
console.error("Missing jQuery");
}
if (!("$TD" in window)){
console.error("Missing $TD");
}
if (!("$TDX" in window)){
console.error("Missing $TDX");
}
//
// Variable: Array of functions called after the website app is loaded.
//
let onAppReady = [];
//
// Variable: DOM object containing the main app element.
//
const app = typeof $ === "function" && $(document.body).children(".js-app");
//
// Function: Prepends code at the beginning of a function. If the prepended function returns true, execution of the original function is cancelled.
//
const prependToFunction = function(func, extension){
return function(){
return extension.apply(this, arguments) === true ? undefined : func.apply(this, arguments);
};
};
//
// Function: Appends code at the end of a function.
//
const appendToFunction = function(func, extension){
return function(){
const res = func.apply(this, arguments);
extension.apply(this, arguments);
return res;
};
};
//
// Function: Triggers an internal debug crash when something is missing.
//
const crashDebug = function(message){
console.error(message);
debugger;
if ("$TD" in window){
$TD.crashDebug(message);
}
}
//
// Function: Throws if an object does not have a specified property.
//
const ensurePropertyExists = function(obj, ...chain){
for(let index = 0; index < chain.length; index++){
if (!obj.hasOwnProperty(chain[index])){
throw "Missing property " + chain[index] + " in chain [obj]." + chain.join(".");
}
obj = obj[chain[index]];
}
};
//
// Function: Returns true if an object has a specified property, otherwise returns false with a debug-only error message.
//
const checkPropertyExists = function(obj, ...chain){
try{
ensurePropertyExists(obj, ...chain);
return true;
}catch(err){
crashDebug(err);
return false;
}
};
//
// Function: Throws if an element does not have a registered jQuery event.
//
const ensureEventExists = function(element, eventName){
if (!(eventName in $._data(element, "events"))){
throw "Missing jQuery event " + eventName + " in " + element.cloneNode().outerHTML;
}
};
//
// Function: Returns a jQuery object but also shows a debug-only error message if no elements are found.
//
const $$ = function(selector, context){
const result = $(selector, context);
if (!result.length){
crashDebug("No elements were found for selector " + selector);
}
return result;
};
//
// Function: Executes a function inside a try-catch to stop it from crashing everything.
//
const execSafe = function(func, fail){
try{
func();
}catch(err){
crashDebug("Caught error in function " + func.name)
fail && fail();
}
};
//
// Function: Returns an object containing data about the column below the cursor.
//
const getHoveredColumn = function(){
const hovered = document.querySelectorAll(":hover");
for(let index = hovered.length - 1; index >= 0; index--){
const ele = hovered[index];
if (ele.tagName === "SECTION" && ele.classList.contains("js-column")){
const obj = TD.controller.columnManager.get(ele.getAttribute("data-column"));
if (obj){
return { ele, obj };
}
}
}
return null;
};
//
// Function: Returns an object containing data about the tweet below the cursor.
//
const getHoveredTweet = function(){
const hovered = document.querySelectorAll(":hover");
for(let index = hovered.length - 1; index >= 0; index--){
const ele = hovered[index];
if (ele.tagName === "ARTICLE" && ele.classList.contains("js-stream-item") && ele.hasAttribute("data-account-key")){
const column = getHoveredColumn();
if (column){
const wrap = column.obj.findChirp(ele.getAttribute("data-key"));
const obj = column.obj.findChirp(ele.getAttribute("data-tweet-id")) || wrap;
if (obj){
return { ele, obj, wrap, column };
}
}
}
}
return null;
};
//
// Function: Retrieves a property of an element with a specified class.
//
const getClassStyleProperty = function(cls, property){
const column = document.createElement("div");
column.classList.add(cls);
column.style.display = "none";
document.body.appendChild(column);
const value = window.getComputedStyle(column).getPropertyValue(property);
document.body.removeChild(column);
return value;
};
//
// Block: Fix columns missing any identifiable attributes to allow individual styles.
//
execSafe(function setupColumnAttrIdentifiers(){
$(document).on("uiColumnRendered", function(e, data){
const icon = data.$column.find(".column-type-icon").first();
return if icon.length !== 1;
const name = Array.prototype.find.call(icon[0].classList, cls => cls.startsWith("icon-"));
return if !name;
data.$column.attr("data-td-icon", name);
data.column._tduck_icon = name;
});
});
//
// Block: Add TweetDuck buttons to the settings menu.
//
onAppReady.push(function setupSettingsDropdown(){
document.querySelector("[data-action='settings-menu']").addEventListener("click", function(){
setTimeout(function(){
const menu = document.querySelector(".js-dropdown-content ul");
return if !menu;
const dividers = menu.querySelectorAll(":scope > li.drp-h-divider");
const target = dividers[dividers.length - 1];
target.insertAdjacentHTML("beforebegin", '<li class="is-selectable" data-tweetduck><a href="#" data-action>TweetDuck</a></li>');
const button = menu.querySelector("[data-tweetduck]");
button.querySelector("a").addEventListener("click", function(){
$TD.openContextMenu();
});
button.addEventListener("mouseenter", function(){
button.classList.add("is-selected");
});
button.addEventListener("mouseleave", function(){
button.classList.remove("is-selected");
})
}, 0);
});
});
//
// Block: Hook into settings object to detect when the settings change, and update html attributes and notification layout.
//
execSafe(function hookTweetDeckSettings(){
ensurePropertyExists(TD, "settings", "getFontSize");
ensurePropertyExists(TD, "settings", "setFontSize");
ensurePropertyExists(TD, "settings", "getTheme");
ensurePropertyExists(TD, "settings", "setTheme");
const doc = document.documentElement;
const refreshSettings = function(){
const fontSizeName = TD.settings.getFontSize();
const themeName = TD.settings.getTheme();
const tags = [
"<html " + Array.prototype.map.call(doc.attributes, ele => `${ele.name}="${ele.value}"`).join(" ") + "><head>"
];
for(let ele of document.head.querySelectorAll("link[rel='stylesheet']:not([data-td-exclude-notification]),meta[charset]")){
tags.push(ele.outerHTML);
}
tags.push("<style type='text/css'>body { background: " + getClassStyleProperty("column-panel", "background-color") + " !important }</style>");
doc.setAttribute("data-td-font", fontSizeName);
doc.setAttribute("data-td-theme", themeName);
$TD.loadNotificationLayout(fontSizeName, tags.join(""));
};
TD.settings.setFontSize = appendToFunction(TD.settings.setFontSize, function(name){
setTimeout(refreshSettings, 0);
});
TD.settings.setTheme = appendToFunction(TD.settings.setTheme, function(name){
setTimeout(refreshSettings, 0);
});
onAppReady.push(refreshSettings);
});
//
// Block: Setup CSS injections.
//
execSafe(function setupStyleInjection(){
const createStyle = function(id, styles){
const ele = document.createElement("style");
ele.id = id;
ele.innerText = styles;
document.head.appendChild(ele);
};
window.TDGF_injectBrowserCSS = function(styles){
if (!document.getElementById("tweetduck-browser-css")){
createStyle("tweetduck-browser-css", styles);
}
};
window.TDGF_reinjectCustomCSS = function(styles){
const prev = document.getElementById("tweetduck-custom-css");
if (prev){
prev.remove();
}
if (styles && styles.length){
createStyle("tweetduck-custom-css", styles);
}
};
});
//
// Block: Setup custom sound notification.
//
window.TDGF_setSoundNotificationData = function(custom, volume){
const audio = document.getElementById("update-sound");
audio.volume = volume / 100;
const sourceId = "tduck-custom-sound-source";
let source = document.getElementById(sourceId);
if (custom && !source){
source = document.createElement("source");
source.id = sourceId;
source.src = "https://ton.twimg.com/tduck/updatesnd";
audio.prepend(source);
}
else if (!custom && source){
audio.removeChild(source);
}
audio.load();
};
//
// Block: Hook into composer event.
//
execSafe(function hookComposerEvents(){
$(document).on("uiDrawerActive uiRwebComposerOptOut", function(e, data){
return if e.type === "uiDrawerActive" && data.activeDrawer !== "compose";
setTimeout(function(){
$(document).trigger("tduckOldComposerActive");
}, 0);
});
});
//
// Block: Setup a top tier account bamboozle scheme.
//
execSafe(function setupAccountLoadHook(){
const realDisplayName = "TweetDuck";
const realAvatar = "https://ton.twimg.com/tduck/avatar";
const accountId = "957608948189880320";
if (checkPropertyExists(TD, "services", "TwitterUser", "prototype", "fromJSONObject")){
const prevFunc = TD.services.TwitterUser.prototype.fromJSONObject;
TD.services.TwitterUser.prototype.fromJSONObject = function(){
const obj = prevFunc.apply(this, arguments);
if (obj.id === accountId){
obj.name = realDisplayName;
obj.emojifiedName = realDisplayName;
obj.profileImageURL = realAvatar;
obj.url = "https://tweetduck.chylex.com";
if (obj.entities && obj.entities.url){
obj.entities.url.urls = [{
url: obj.url,
expanded_url: obj.url,
display_url: "tweetduck.chylex.com",
indices: [ 0, 23 ]
}];
}
}
return obj;
};
}
if (checkPropertyExists(TD, "services", "TwitterClient", "prototype", "typeaheadSearch")){
const prevFunc = TD.services.TwitterClient.prototype.typeaheadSearch;
TD.services.TwitterClient.prototype.typeaheadSearch = function(data, onSuccess, onError){
if (data.query && data.query.toLowerCase().endsWith("tweetduck")){
data.query = "TryMyAwesomeApp";
}
return prevFunc.call(this, data, function(result){
for(let user of result.users){
if (user.id_str === accountId){
user.name = realDisplayName;
user.profile_image_url = realAvatar;
user.profile_image_url_https = realAvatar;
break;
}
}
onSuccess.apply(this, arguments);
}, onError);
};
}
});
//
// Block: Work around clipboard HTML formatting.
//
document.addEventListener("copy", function(){
window.setTimeout($TD.fixClipboard, 0);
});
//
// Block: Fix OS name and add ID to the document for priority CSS selectors.
//
(function(){
const doc = document.documentElement;
if (checkPropertyExists(TD, "util", "getOSName")){
TD.util.getOSName = function(){
return "windows";
};
doc.classList.remove("os-");
doc.classList.add("os-windows");
}
doc.id = "tduck";
})();
//
// Block: Disable TweetDeck metrics.
//
if (checkPropertyExists(TD, "metrics")){
const noop = function(){};
TD.metrics.inflate = noop;
TD.metrics.inflateMetricTriple = noop;
TD.metrics.log = noop;
TD.metrics.makeKey = noop;
TD.metrics.send = noop;
}
onAppReady.push(function disableMetrics(){
const data = $._data(window);
delete data.events["metric"];
delete data.events["metricsFlush"];
});
//
// Block: Import scripts.
//
#import "scripts/browser.globals.js"
#import "scripts/browser.features.js"
#import "scripts/browser.tweaks.js"
//
// Block: Register the TD.ready event, finish initialization, and load plugins.
//
$(document).one("TD.ready", function(){
onAppReady.forEach(func => execSafe(func));
onAppReady = null;
if (window.TD_PLUGINS){
window.TD_PLUGINS.onReady();
}
});
//
// Block: Ensure window.jQuery is available.
//
window.jQuery = $;
//
// Block: Skip the initial pre-login page.
//
if (checkPropertyExists(TD, "controller", "init", "showLogin")){
TD.controller.init.showLogin = function(){
location.href = "https://twitter.com/login?hide_message=true&redirect_after_login=https%3A%2F%2Ftweetdeck.twitter.com%2F%3Fvia_twitter_login%3Dtrue";
};
}
})(window.$ || null, window.TD || {});

View File

@ -1,11 +0,0 @@
<div id="tweetduck-conn-issues" class="Layer NotificationListLayer">
<ul class="NotificationList">
<li class="Notification Notification--red" style="height:63px;">
<div class="Notification-inner">
<div class="Notification-icon"><span class="Icon Icon--medium Icon--circleError"></span></div>
<div class="Notification-content"><div class="Notification-body">Experiencing connection issues</div></div>
<button type="button" class="Notification-closeButton" aria-label="Close"><span class="Icon Icon--smallest Icon--close" aria-hidden="true"></span></button>
</div>
</li>
</ul>
</div>

View File

@ -1,6 +0,0 @@
<svg id="td-compose-drawer-pin" viewBox="0 0 24 24" class="icon js-show-tip" data-original-title="Stay open" data-tooltip-position="left">
<path d="M9.884,16.959l3.272,0.001l-0.82,4.568l-1.635,0l-0.817,-4.569Z"/>
<rect x="8.694" y="7.208" width="5.652" height="7.445"/>
<path d="M16.877,17.448c0,-1.908 -1.549,-3.456 -3.456,-3.456l-3.802,0c-1.907,0 -3.456,1.548 -3.456,3.456l10.714,0Z"/>
<path d="M6.572,5.676l2.182,2.183l5.532,0l2.182,-2.183l0,-1.455l-9.896,0l0,1.455Z"/>
</svg>

Before

(image error) Size: 488 B

View File

@ -1,670 +0,0 @@
(function(){
//
// Function: Event callback for a new tweet.
//
const onNewTweet = (function(){
const recentMessages = new Set();
const recentTweets = new Set();
let recentTweetTimer = null;
const resetRecentTweets = () => {
recentTweetTimer = null;
recentTweets.clear();
};
const startRecentTweetTimer = () => {
recentTweetTimer && window.clearTimeout(recentTweetTimer);
recentTweetTimer = window.setTimeout(resetRecentTweets, 20000);
};
const checkTweetCache = (set, id) => {
return true if set.has(id);
if (set.size > 50){
set.clear();
}
set.add(id);
return false;
};
const isSensitive = (tweet) => {
const main = tweet.getMainTweet && tweet.getMainTweet();
return true if main && main.possiblySensitive; // TODO these don't show media badges when hiding sensitive media
const related = tweet.getRelatedTweet && tweet.getRelatedTweet();
return true if related && related.possiblySensitive;
const quoted = tweet.quotedTweet;
return true if quoted && quoted.possiblySensitive;
return false;
};
const fixMedia = (html, media) => {
return html.find("a[data-media-entity-id='" + media.mediaId + "'], .media-item").first().removeClass("is-zoomable").css("background-image", 'url("' + media.small() + '")');
};
return function(column, tweet){
if (tweet instanceof TD.services.TwitterConversation || tweet instanceof TD.services.TwitterConversationMessageEvent){
return if checkTweetCache(recentMessages, tweet.id);
}
else{
return if checkTweetCache(recentTweets, tweet.id);
}
startRecentTweetTimer();
if (column.model.getHasNotification()){
const sensitive = isSensitive(tweet);
const previews = $TDX.notificationMediaPreviews && (!sensitive || TD.settings.getDisplaySensitiveMedia());
// TODO new cards don't have either previews or links
const html = $(tweet.render({
withFooter: false,
withTweetActions: false,
withMediaPreview: true,
isMediaPreviewOff: !previews,
isMediaPreviewSmall: previews,
isMediaPreviewLarge: false,
isMediaPreviewCompact: false,
isMediaPreviewInQuoted: previews,
thumbSizeClass: "media-size-medium",
mediaPreviewSize: "medium"
}));
html.find("footer").last().remove(); // apparently withTweetActions breaks for certain tweets, nice
html.find(".js-quote-detail").removeClass("is-actionable margin-b--8"); // prevent quoted tweets from changing the cursor and reduce bottom margin
if (previews){
html.find(".reverse-image-search").remove();
const container = html.find(".js-media");
for(let media of tweet.getMedia()){
fixMedia(container, media);
}
if (tweet.quotedTweet){
for(let media of tweet.quotedTweet.getMedia()){
fixMedia(container, media).addClass("media-size-medium");
}
}
}
else if (tweet instanceof TD.services.TwitterActionOnTweet){
html.find(".js-media").remove();
}
html.find("a[data-full-url]").each(function(){ // bypass t.co on all links and fix tooltips
this.href = this.getAttribute("data-full-url");
this.removeAttribute("title");
});
html.find("a[href='#']").each(function(){ // remove <a> tags around links that don't lead anywhere (such as account names the tweet replied to)
this.outerHTML = this.innerHTML;
});
html.find("p.link-complex-target").filter(function(){
return $(this).text() === "Show this thread";
}).first().each(function(){
this.id = "tduck-show-thread";
const moveBefore = html.find(".tweet-body > .js-media, .tweet-body > .js-media-preview-container, .quoted-tweet");
if (moveBefore){
$(this).css("margin-top", "5px").removeClass("margin-b--5").parent("span").detach().insertBefore(moveBefore);
}
});
if (tweet.quotedTweet){
html.find("p.txt-mute").filter(function(){
return $(this).text() === "Show this thread";
}).first().remove();
}
const type = tweet.getChirpType();
if (type === "follow"){
html.find(".js-user-actions-menu").parent().remove();
html.find(".account-bio").removeClass("padding-t--5").css("padding-top", "2px");
}
else if ((type.startsWith("favorite") || type.startsWith("retweet")) && tweet.isAboutYou()){
html.children().first().addClass("td-notification-padded");
}
else if (type.includes("list_member")){
html.children().first().addClass("td-notification-padded td-notification-padded-alt");
html.find(".activity-header").css("margin-top", "2px");
html.find(".avatar").first().css("margin-bottom", "0");
}
if (sensitive){
html.find(".media-badge").each(function(){
$(this)[0].lastChild.textContent += " (possibly sensitive)";
});
}
const source = tweet.getRelatedTweet();
const duration = source ? source.text.length + (source.quotedTweet ? source.quotedTweet.text.length : 0) : tweet.text.length;
const chirpId = source ? source.id : "";
const tweetUrl = source ? source.getChirpURL() : "";
const quoteUrl = source && source.quotedTweet ? source.quotedTweet.getChirpURL() : "";
$TD.onTweetPopup(column.model.privateState.apiid, chirpId, window.TDGF_getColumnName(column), html.html(), duration, tweetUrl, quoteUrl);
}
if (column.model.getHasSound()){
$TD.onTweetSound();
}
};
})();
//
// Block: Enable popup notifications.
//
execSafe(function hookDesktopNotifications(){
ensurePropertyExists(TD, "controller", "notifications");
TD.controller.notifications.hasNotifications = function(){
return true;
};
TD.controller.notifications.isPermissionGranted = function(){
return true;
};
$.subscribe("/notifications/new", function(obj){
for(let index = obj.items.length - 1; index >= 0; index--){
onNewTweet(obj.column, obj.items[index]);
}
});
});
//
// Block: Fix DM notifications not showing if the conversation is open.
//
if (checkPropertyExists(TD, "vo", "Column", "prototype", "mergeMissingChirps")){
TD.vo.Column.prototype.mergeMissingChirps = prependToFunction(TD.vo.Column.prototype.mergeMissingChirps, function(e){
const model = this.model;
if (model && model.state && model.state.type === "privateMe" && !this.notificationsDisabled && e.poller.feed.managed){
const unread = [];
for(let chirp of e.chirps){
if (Array.isArray(chirp.messages)){
Array.prototype.push.apply(unread, chirp.messages.filter(message => message.read === false));
}
}
if (unread.length > 0){
if (checkPropertyExists(TD, "util", "chirpReverseColumnSort")){
unread.sort(TD.util.chirpReverseColumnSort);
}
for(let message of unread){
onNewTweet(this, message);
}
// TODO sound notifications are borked as well
// TODO figure out what to do with missed notifications at startup
}
}
});
}
})();
//
// Block: Mute sound notifications.
//
HTMLAudioElement.prototype.play = prependToFunction(HTMLAudioElement.prototype.play, function(){
return $TDX.muteNotifications;
});
//
// Block: Add additional link information to context menu.
//
execSafe(function setupLinkContextMenu(){
$(document.body).delegate("a", "contextmenu", function(){
const me = $(this)[0];
if (me.classList.contains("js-media-image-link")){
const hovered = getHoveredTweet();
return if !hovered;
const tweet = hovered.obj.hasMedia() ? hovered.obj : hovered.obj.quotedTweet;
const media = tweet.getMedia().find(media => media.mediaId === me.getAttribute("data-media-entity-id"));
if ((media.isVideo && media.service === "twitter") || media.isAnimatedGif){
$TD.setRightClickedLink("video", media.chooseVideoVariant().url);
}
else{
$TD.setRightClickedLink("image", media.large());
}
}
else if (me.classList.contains("js-gif-play")){
$TD.setRightClickedLink("video", $(this).closest(".js-media-gif-container").find("video").attr("src"));
}
else if (me.hasAttribute("data-full-url")){
$TD.setRightClickedLink("link", me.getAttribute("data-full-url"));
}
});
});
//
// Block: Add tweet-related options to context menu.
//
execSafe(function setupTweetContextMenu(){
ensurePropertyExists(TD, "controller", "columnManager", "get");
ensurePropertyExists(TD, "services", "ChirpBase", "TWEET");
ensurePropertyExists(TD, "services", "TwitterActionFollow");
const processMedia = function(chirp){
return chirp.getMedia().filter(item => !item.isAnimatedGif).map(item => item.entity.media_url_https + ":small").join(";");
};
app.delegate("section.js-column", "contextmenu", function(){
const hovered = getHoveredTweet();
return if !hovered;
const tweet = hovered.obj;
const quote = tweet.quotedTweet;
if (tweet.chirpType === TD.services.ChirpBase.TWEET){
const tweetUrl = tweet.getChirpURL();
const quoteUrl = quote && quote.getChirpURL();
const chirpAuthors = quote ? [ tweet.getMainUser().screenName, quote.getMainUser().screenName ].join(";") : tweet.getMainUser().screenName;
const chirpImages = tweet.hasImage() ? processMedia(tweet) : quote && quote.hasImage() ? processMedia(quote) : "";
$TD.setRightClickedChirp(tweetUrl || "", quoteUrl || "", chirpAuthors, chirpImages);
}
else if (tweet instanceof TD.services.TwitterActionFollow){
$TD.setRightClickedLink("link", tweet.following.getProfileURL());
}
});
});
//
// Block: Expand shortened links on hover or display tooltip.
//
execSafe(function setupLinkExpansionOrTooltip(){
let prevMouseX = -1, prevMouseY = -1;
let tooltipTimer, tooltipDisplayed;
$(document.body).delegate("a[data-full-url]", {
mouseenter: function(){
const me = $(this);
const text = me.text();
return if text.charCodeAt(text.length - 1) !== 8230 && text.charCodeAt(0) !== 8230; // horizontal ellipsis
if ($TDX.expandLinksOnHover){
tooltipTimer = window.setTimeout(function(){
me.attr("td-prev-text", text);
me.text(me.attr("data-full-url").replace(/^https?:\/\/(www\.)?/, ""));
}, 200);
}
else{
me.removeAttr("title");
tooltipTimer = window.setTimeout(function(){
$TD.displayTooltip(me.attr("data-full-url"));
tooltipDisplayed = true;
}, 400);
}
},
mouseleave: function(){
const me = $(this)[0];
if (me.hasAttribute("td-prev-text")){
me.innerText = me.getAttribute("td-prev-text");
}
window.clearTimeout(tooltipTimer);
if (tooltipDisplayed){
tooltipDisplayed = false;
$TD.displayTooltip(null);
}
},
mousemove: function(e){
if (tooltipDisplayed && (prevMouseX !== e.clientX || prevMouseY !== e.clientY)){
$TD.displayTooltip($(this).attr("data-full-url"));
prevMouseX = e.clientX;
prevMouseY = e.clientY;
}
}
});
});
//
// Block: Support for extra mouse buttons.
//
execSafe(function supportExtraMouseButtons(){
const tryClickSelector = function(selector, parent){
return $(selector, parent).click().length;
};
const tryCloseModal1 = function(){
const modal = $("#open-modal");
return modal.is(":visible") && tryClickSelector("a.mdl-dismiss", modal);
};
const tryCloseModal2 = function(){
const modal = $(".js-modals-container");
return modal.length && tryClickSelector("a.mdl-dismiss", modal);
};
const tryCloseHighlightedColumn = function(){
const column = getHoveredColumn();
return false if !column;
const ele = $(column.ele);
return ((ele.is(".is-shifted-2") && tryClickSelector(".js-tweet-social-proof-back", ele)) || (ele.is(".is-shifted-1") && tryClickSelector(".js-column-back", ele)));
};
window.TDGF_onMouseClickExtra = function(button){
if (button === 1){ // back button
tryClickSelector(".is-shifted-2 .js-tweet-social-proof-back", ".js-modal-panel") ||
tryClickSelector(".is-shifted-1 .js-column-back", ".js-modal-panel") ||
tryCloseModal1() ||
tryCloseModal2() ||
tryClickSelector(".js-inline-compose-close") ||
tryCloseHighlightedColumn() ||
tryClickSelector(".js-app-content.is-open .js-drawer-close:visible") ||
tryClickSelector(".is-shifted-2 .js-tweet-social-proof-back, .is-shifted-2 .js-dm-participants-back") ||
$(".is-shifted-1 .js-column-back").click();
}
else if (button === 2){ // forward button
const hovered = getHoveredTweet();
if (hovered){
$(hovered.ele).children().first().click();
}
}
};
});
//
// Block: Allow drag & drop behavior for dropping links on columns to open their detail view.
//
execSafe(function supportDragDropOverColumns(){
const regexTweet = /^https?:\/\/twitter\.com\/[A-Za-z0-9_]+\/status\/(\d+)\/?\??/;
const regexAccount = /^https?:\/\/twitter\.com\/(?!signup$|tos$|privacy$|search$|search-)([^/?]+)\/?$/;
let dragType = false;
const events = {
dragover: function(e){
e.originalEvent.dataTransfer.dropEffect = dragType ? "all" : "none";
e.preventDefault();
e.stopPropagation();
},
drop: function(e){
const url = e.originalEvent.dataTransfer.getData("URL");
if (dragType === "tweet"){
const match = regexTweet.exec(url);
if (match.length === 2){
const column = TD.controller.columnManager.get($(this).attr("data-column"));
if (column){
TD.controller.clients.getPreferredClient().show(match[1], function(chirp){
TD.ui.updates.showDetailView(column, chirp, column.findChirp(chirp) || chirp);
$(document).trigger("uiGridClearSelection");
}, function(){
alert("error|Could not retrieve the requested tweet.");
});
}
}
}
else if (dragType === "account"){
const match = regexAccount.exec(url);
if (match.length === 2){
$(document).trigger("uiShowProfile", { id: match[1] });
}
}
e.preventDefault();
e.stopPropagation();
}
};
const selectors = {
tweet: "section.js-column",
account: app
};
window.TDGF_onGlobalDragStart = function(type, data){
if (dragType){
app.undelegate(selectors[dragType], events);
dragType = null;
}
if (type === "link"){
dragType = regexTweet.test(data) ? "tweet" : regexAccount.test(data) ? "account": null;
app.delegate(selectors[dragType], events);
}
};
});
//
// Block: Make middle click on tweet reply icon open the compose drawer, retweet icon trigger a quote, and favorite icon open a 'Like from accounts...' modal.
//
execSafe(function supportMiddleClickTweetActions(){
app.delegate(".tweet-action,.tweet-detail-action", "auxclick", function(e){
return if e.which !== 2;
const column = TD.controller.columnManager.get($(this).closest("section.js-column").attr("data-column"));
return if !column;
const ele = $(this).closest("article");
const tweet = column.findChirp(ele.attr("data-tweet-id")) || column.findChirp(ele.attr("data-key"));
return if !tweet;
switch($(this).attr("rel")){
case "reply":
const main = tweet.getMainTweet();
$(document).trigger("uiDockedComposeTweet", {
type: "reply",
from: [ tweet.account.getKey() ],
inReplyTo: {
id: tweet.id,
htmlText: main.htmlText,
user: {
screenName: main.user.screenName,
name: main.user.name,
profileImageURL: main.user.profileImageURL
}
},
mentions: tweet.getReplyUsers(),
element: ele
});
break;
case "favorite":
$(document).trigger("uiShowFavoriteFromOptions", { tweet });
break;
case "retweet":
TD.controller.stats.quoteTweet();
$(document).trigger("uiComposeTweet", {
type: "tweet",
from: [ tweet.account.getKey() ],
quotedTweet: tweet.getMainTweet(),
element: ele // triggers reply-account plugin
});
break;
default:
return;
}
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
});
});
//
// Block: Add a pin icon to make tweet compose drawer stay open.
//
execSafe(function setupStayOpenPin(){
$(document).on("tduckOldComposerActive", function(e){
const ele = $(`#import "markup/pin.html"`).appendTo(".js-docked-compose .js-compose-header");
ele.click(function(){
if (TD.settings.getComposeStayOpen()){
ele.css("transform", "rotate(0deg)");
TD.settings.setComposeStayOpen(false);
}
else{
ele.css("transform", "rotate(90deg)");
TD.settings.setComposeStayOpen(true);
}
});
if (TD.settings.getComposeStayOpen()){
ele.css("transform", "rotate(90deg)");
}
});
});
//
// Block: Make submitting search queries while holding Ctrl or middle-clicking the search icon open the search externally.
//
onAppReady.push(function setupSearchTriggerHook(){
const openSearchExternally = function(event, input){
$TD.openBrowser("https://twitter.com/search/?q=" + encodeURIComponent(input.val() || ""));
event.preventDefault();
event.stopPropagation();
input.val("").blur();
app.click(); // unfocus everything
};
$$(".js-app-search-input").keydown(function(e){
(e.ctrlKey && e.keyCode === 13) && openSearchExternally(e, $(this)); // enter
});
$$(".js-perform-search").on("click auxclick", function(e){
(e.ctrlKey || e.button === 1) && openSearchExternally(e, $(".js-app-search-input:visible"));
}).each(function(){
window.TDGF_prioritizeNewestEvent($(this)[0], "click");
});
$$("[data-action='show-search']").on("click auxclick", function(e){
(e.ctrlKey || e.button === 1) && openSearchExternally(e, $());
});
});
//
// Block: Setup video player hooks.
//
execSafe(function setupVideoPlayer(){
const getGifLink = function(ele){
return ele.attr("src") || ele.children("source[video-src]").first().attr("video-src");
};
const getVideoTweetLink = function(obj){
let parent = obj.closest(".js-tweet").first();
let link = (parent.hasClass("tweet-detail") ? parent.find("a[rel='url']") : parent.find("time").first().children("a")).first();
return link.attr("href");
};
const getUsername = function(tweet){
return tweet && (tweet.quotedTweet || tweet).getMainUser().screenName;
};
app.delegate(".js-gif-play", {
click: function(e){
let src = !e.ctrlKey && getGifLink($(this).closest(".js-media-gif-container").find("video"));
let tweet = getVideoTweetLink($(this));
if (src){
let hovered = getHoveredTweet();
window.TDGF_playVideo(src, tweet, getUsername(hovered && hovered.obj));
}
else{
$TD.openBrowser(tweet);
}
e.stopPropagation();
},
mousedown: function(e){
if (e.button === 1){
e.preventDefault();
}
},
mouseup: function(e){
if (e.button === 1){
$TD.openBrowser(getVideoTweetLink($(this)));
e.preventDefault();
}
}
});
window.TDGF_injectMustache("status/media_thumb.mustache", "append", "is-gif", " is-paused");
TD.mustaches["media/native_video.mustache"] = '<div class="js-media-gif-container media-item nbfc is-video" style="background-image:url({{imageSrc}})"><video class="js-media-gif media-item-gif full-width block {{#isPossiblySensitive}}is-invisible{{/isPossiblySensitive}}" loop src="{{videoUrl}}"></video><a class="js-gif-play pin-all is-actionable">{{> media/video_overlay}}</a></div>';
ensurePropertyExists(TD, "components", "MediaGallery", "prototype", "_loadTweet");
ensurePropertyExists(TD, "components", "BaseModal", "prototype", "setAndShowContainer");
ensurePropertyExists(TD, "ui", "Column", "prototype", "playGifIfNotManuallyPaused");
let cancelModal = false;
TD.components.MediaGallery.prototype._loadTweet = appendToFunction(TD.components.MediaGallery.prototype._loadTweet, function(){
const media = this.chirp.getMedia().find(media => media.mediaId === this.clickedMediaEntityId);
if (media && media.isVideo && media.service === "twitter"){
window.TDGF_playVideo(media.chooseVideoVariant().url, this.chirp.getChirpURL(), getUsername(this.chirp));
cancelModal = true;
}
});
TD.components.BaseModal.prototype.setAndShowContainer = prependToFunction(TD.components.BaseModal.prototype.setAndShowContainer, function(){
if (cancelModal){
cancelModal = false;
return true;
}
});
TD.ui.Column.prototype.playGifIfNotManuallyPaused = function(){};
});
//
// Block: Detect and notify about connection issues.
//
(function(){
const onConnectionError = function(){
return if $("#tweetduck-conn-issues").length;
const ele = $(`#import "markup/offline.html"`).appendTo(document.body);
ele.find("button").click(function(){
ele.fadeOut(200);
});
};
const onConnectionFine = function(){
const ele = $("#tweetduck-conn-issues");
ele.fadeOut(200, function(){
ele.remove();
});
};
window.addEventListener("offline", onConnectionError);
window.addEventListener("online", onConnectionFine);
})();

View File

@ -1,279 +0,0 @@
//
// Functions: Responds to updating $TDX properties.
//
(function(){
let callbacks = [];
window.TDGF_registerPropertyUpdateCallback = function(callback){
callbacks.push(callback);
};
window.TDGF_onPropertiesUpdated = function(){
callbacks.forEach(func => func($TDX));
};
})();
//
// Function: Injects custom HTML into mustache templates.
//
window.TDGF_injectMustache = function(name, operation, search, custom){
let replacement;
switch(operation){
case "replace": replacement = custom; break;
case "append": replacement = search + custom; break;
case "prepend": replacement = custom + search; break;
default: throw "Invalid mustache injection operation. Only 'replace', 'append', 'prepend' are supported.";
}
const prev = TD.mustaches && TD.mustaches[name];
if (!prev){
crashDebug("Mustache injection is referencing an invalid mustache: " + name);
return false;
}
TD.mustaches[name] = prev.replace(search, replacement);
if (prev === TD.mustaches[name]){
crashDebug("Mustache injection had no effect: " + name);
return false;
}
return true;
};
//
// Function: Pushes the newest jQuery event to the beginning of the event handler list.
//
window.TDGF_prioritizeNewestEvent = function(element, event){
const events = $._data(element, "events");
const handlers = events[event];
const newHandler = handlers[handlers.length - 1];
for(let index = handlers.length - 1; index > 0; index--){
handlers[index] = handlers[index - 1];
}
handlers[0] = newHandler;
};
//
// Function: Returns a display name for a column object.
//
window.TDGF_getColumnName = (function(){
const titles = {
"icon-home": "Home",
"icon-mention": "Mentions",
"icon-message": "Messages",
"icon-notifications": "Notifications",
"icon-follow": "Followers",
"icon-activity": "Activity",
"icon-favorite": "Likes",
"icon-user": "User",
"icon-search": "Search",
"icon-list": "List",
"icon-custom-timeline": "Timeline",
"icon-dataminr": "Dataminr",
"icon-play-video": "Live video",
"icon-schedule": "Scheduled"
};
return function(column){
return titles[column._tduck_icon] || "";
};
})();
//
// Function: Adds a search column with the specified query.
//
onAppReady.push(() => execSafe(function setupSearchFunction(){
const context = $._data(document, "events")["uiSearchInputSubmit"][0].handler.context;
window.TDGF_performSearch = function(query){
context.performSearch({ query, tduckResetInput: true });
};
}, function(){
window.TDGF_performSearch = function(){
alert("error|This feature is not available due to an internal error.");
};
}));
//
// Function: Plays sound notification.
//
window.TDGF_playSoundNotification = function(){
document.getElementById("update-sound").play();
};
//
// Function: Plays video using the internal player.
//
window.TDGF_playVideo = function(videoUrl, tweetUrl, username){
return if !videoUrl;
$TD.playVideo(videoUrl, tweetUrl || videoUrl, username || null, function(){
$('<div id="td-video-player-overlay" class="ovl" style="display:block"></div>').on("click contextmenu", function(){
$TD.stopVideo();
}).appendTo(app);
});
};
//
// Function: Shows tweet detail, used in notification context menu.
//
execSafe(function setupShowTweetDetail(){
ensurePropertyExists(TD, "ui", "updates", "showDetailView");
ensurePropertyExists(TD, "controller", "columnManager", "showColumn");
ensurePropertyExists(TD, "controller", "columnManager", "getByApiid");
ensurePropertyExists(TD, "controller", "clients", "getPreferredClient");
const showTweetDetailInternal = function(column, chirp){
TD.ui.updates.showDetailView(column, chirp, column.findChirp(chirp) || chirp);
TD.controller.columnManager.showColumn(column.model.privateState.key);
$(document).trigger("uiGridClearSelection");
};
window.TDGF_showTweetDetail = function(columnId, chirpId, fallbackUrl){
if (!TD.ready){
onAppReady.push(function(){
window.TDGF_showTweetDetail(columnId, chirpId, fallbackUrl);
});
return;
}
const column = TD.controller.columnManager.getByApiid(columnId);
if (!column){
if (confirm("error|The column which contained the tweet no longer exists. Would you like to open the tweet in your browser instead?")){
$TD.openBrowser(fallbackUrl);
}
return;
}
const chirp = column.findMostInterestingChirp(chirpId);
if (chirp){
showTweetDetailInternal(column, chirp);
}
else{
TD.controller.clients.getPreferredClient().show(chirpId, function(chirp){
showTweetDetailInternal(column, chirp);
}, function(){
if (confirm("error|Could not retrieve the requested tweet. Would you like to open the tweet in your browser instead?")){
$TD.openBrowser(fallbackUrl);
}
});
}
};
}, function(){
window.TDGF_showTweetDetail = function(){
alert("error|This feature is not available due to an internal error.");
};
});
//
// Function: Screenshots tweet to clipboard.
//
execSafe(function setupTweetScreenshot(){
window.TDGF_triggerScreenshot = function(){
const hovered = getHoveredTweet();
return if !hovered;
const columnWidth = $(hovered.column.ele).width();
const tweet = hovered.wrap || hovered.obj;
const html = $(tweet.render({
withFooter: false,
withTweetActions: false,
isInConvo: false,
isFavorite: false,
isRetweeted: false, // keeps retweet mark above tweet
isPossiblySensitive: false,
mediaPreviewSize: hovered.column.obj.getMediaPreviewSize()
}));
html.find("footer").last().remove(); // apparently withTweetActions breaks for certain tweets, nice
html.find(".td-screenshot-remove").remove();
html.find("p.link-complex-target,p.txt-mute").filter(function(){
return $(this).text() === "Show this thread";
}).remove();
html.addClass($(document.documentElement).attr("class"));
html.addClass($(document.body).attr("class"));
html.css("background-color", getClassStyleProperty("stream-item", "background-color"));
html.css("border", "none");
for(let selector of [ ".js-quote-detail", ".js-media-preview-container", ".js-media" ]){
const ele = html.find(selector);
if (ele.length){
ele[0].style.setProperty("margin-bottom", "2px", "important");
break;
}
}
const gif = html.find(".js-media-gif-container");
if (gif.length){
gif.css("background-image", 'url("'+tweet.getMedia()[0].small()+'")');
}
const type = tweet.getChirpType();
if ((type.startsWith("favorite") || type.startsWith("retweet")) && tweet.isAboutYou()){
html.addClass("td-notification-padded");
}
$TD.screenshotTweet(html[0].outerHTML, columnWidth);
};
}, function(){
window.TDGF_triggerScreenshot = function(){
alert("error|This feature is not available due to an internal error.");
};
});
//
// Function: Apply ROT13 to input selection.
//
window.TDGF_applyROT13 = function(){
const ele = document.activeElement;
return if !ele || !ele.value;
const selection = ele.value.substring(ele.selectionStart, ele.selectionEnd);
return if !selection;
document.execCommand("insertText", false, selection.replace(/[a-zA-Z]/g, function(chr){
const code = chr.charCodeAt(0);
const start = code <= 90 ? 65 : 97;
return String.fromCharCode(start + (code - start + 13) % 26);
}));
};
//
// Function: Reloads all columns.
//
if (checkPropertyExists(TD, "controller", "columnManager", "getAll")){
window.TDGF_reloadColumns = function(){
Object.values(TD.controller.columnManager.getAll()).forEach(column => column.reloadTweets());
};
}
else{
window.TDGF_reloadColumns = function(){};
}
//
// Function: Reloads the website with memory cleanup.
//
window.TDGF_reload = function(){
window.gc && window.gc();
window.location.reload();
window.TDGF_reload = function(){}; // redefine to prevent reloading multiple times
};

View File

@ -1,468 +0,0 @@
//
// Block: Paste images when tweeting.
//
onAppReady.push(function supportImagePaste(){
const uploader = $._data(document, "events")["uiComposeAddImageClick"][0].handler.context;
app.delegate(".js-compose-text,.js-reply-tweetbox,.td-detect-image-paste", "paste", function(e){
for(let item of e.originalEvent.clipboardData.items){
if (item.type.startsWith("image/")){
if (!$(this).closest(".rpl").find(".js-reply-popout").click().length){ // popout direct messages
return if $(".js-add-image-button").is(".is-disabled"); // tweetdeck does not check upload count properly
}
uploader.addFilesToUpload([ item.getAsFile() ]);
break;
}
}
});
});
//
// Block: Allow changing first day of week in date picker.
//
if (checkPropertyExists($, "tools", "dateinput", "conf", "firstDay")){
$.tools.dateinput.conf.firstDay = $TDX.firstDayOfWeek;
onAppReady.push(function setupDatePickerFirstDayCallback(){
window.TDGF_registerPropertyUpdateCallback(function($TDX){
$.tools.dateinput.conf.firstDay = $TDX.firstDayOfWeek;
});
});
}
//
// Block: Override language used for translations.
//
if (checkPropertyExists(TD, "languages", "getSystemLanguageCode")){
const prevFunc = TD.languages.getSystemLanguageCode;
TD.languages.getSystemLanguageCode = function(returnShortCode){
return returnShortCode ? ($TDX.translationTarget || "en") : prevFunc.apply(this, arguments);
};
}
//
// Block: Add missing languages for Bing Translator (Bengali, Icelandic, Tagalog, Tamil, Telugu, Urdu).
//
execSafe(function addMissingTranslationLanguages(){
ensurePropertyExists(TD, "languages", "getSupportedTranslationSourceLanguages");
const newCodes = [ "bn", "is", "tl", "ta", "te", "ur" ];
const codeSet = new Set(TD.languages.getSupportedTranslationSourceLanguages());
for(const lang of newCodes){
codeSet.add(lang);
}
const codeList = [...codeSet];
TD.languages.getSupportedTranslationSourceLanguages = function(){
return codeList;
};
});
//
// Block: Bypass t.co when clicking/dragging links, media, and in user profiles.
//
execSafe(function setupShortenerBypass(){
$(document.body).delegate("a[data-full-url]", "click auxclick", function(e){
// event.which seems to be borked in auxclick
// tweet links open directly in the column
if ((e.button === 0 || e.button === 1) && $(this).attr("rel") !== "tweet"){
$TD.openBrowser($(this).attr("data-full-url"));
e.preventDefault();
}
});
$(document.body).delegate("a[data-full-url]", "dragstart", function(e){
const url = $(this).attr("data-full-url");
const data = e.originalEvent.dataTransfer;
data.clearData();
data.setData("text/uri-list", url);
data.setData("text/plain", url);
data.setData("text/html", `<a href="${url}">${url}</a>`);
});
if (checkPropertyExists(TD, "services", "TwitterStatus", "prototype", "_generateHTMLText")){
TD.services.TwitterStatus.prototype._generateHTMLText = prependToFunction(TD.services.TwitterStatus.prototype._generateHTMLText, function(){
const card = this.card;
const entities = this.entities;
return if !(card && entities);
const urls = entities.urls;
return if !(urls && urls.length);
const shortUrl = card.url;
const urlObj = entities.urls.find(obj => obj.url === shortUrl && obj.expanded_url);
if (urlObj){
const expandedUrl = urlObj.expanded_url;
card.url = expandedUrl;
const values = card.binding_values;
if (values && values.card_url){
values.card_url.string_value = expandedUrl;
}
}
});
}
if (checkPropertyExists(TD, "services", "TwitterMedia", "prototype", "fromMediaEntity")){
const prevFunc = TD.services.TwitterMedia.prototype.fromMediaEntity;
TD.services.TwitterMedia.prototype.fromMediaEntity = function(){
const obj = prevFunc.apply(this, arguments);
const e = arguments[0];
if (e.expanded_url){
if (obj.url === obj.shortUrl){
obj.shortUrl = e.expanded_url;
}
obj.url = e.expanded_url;
}
return obj;
};
}
if (checkPropertyExists(TD, "services", "TwitterUser", "prototype", "fromJSONObject")){
const prevFunc = TD.services.TwitterUser.prototype.fromJSONObject;
TD.services.TwitterUser.prototype.fromJSONObject = function(){
const obj = prevFunc.apply(this, arguments);
const e = arguments[0].entities;
if (e && e.url && e.url.urls && e.url.urls.length && e.url.urls[0].expanded_url){
obj.url = e.url.urls[0].expanded_url;
}
return obj;
};
}
});
//
// Block: Fix youtu.be previews not showing up for https links.
//
execSafe(function fixYouTubePreviews(){
ensurePropertyExists(TD, "services", "TwitterMedia");
const media = TD.services.TwitterMedia;
ensurePropertyExists(media, "YOUTUBE_TINY_RE");
ensurePropertyExists(media, "YOUTUBE_LONG_RE");
ensurePropertyExists(media, "YOUTUBE_RE");
ensurePropertyExists(media, "SERVICES", "youtube");
media.YOUTUBE_TINY_RE = new RegExp(media.YOUTUBE_TINY_RE.source.replace("http:", "https?:"));
media.YOUTUBE_RE = new RegExp(media.YOUTUBE_LONG_RE.source + "|" + media.YOUTUBE_TINY_RE.source);
media.SERVICES["youtube"] = media.YOUTUBE_RE;
});
//
// Block: Refocus the textbox after switching accounts.
//
onAppReady.push(function setupAccountSwitchRefocus(){
const refocusInput = function(){
document.querySelector(".js-docked-compose .js-compose-text").focus();
};
const accountItemClickEvent = function(e){
setTimeout(refocusInput, 0);
};
$(document).on("tduckOldComposerActive", function(e){
$$(".js-account-list", ".js-docked-compose").delegate(".js-account-item", "click", accountItemClickEvent);
});
});
//
// Block: Fix docked composer not re-focusing after Alt+Tab & image upload.
//
onAppReady.push(function fixDockedComposerRefocus(){
$(document).on("tduckOldComposerActive", function(e){
const ele = $$(".js-compose-text", ".js-docked-compose");
const node = ele[0];
let cancelBlur = false;
ele.on("blur", function(e){
cancelBlur = true;
setTimeout(function(){ cancelBlur = false; }, 0);
});
window.TDGF_prioritizeNewestEvent(node, "blur");
node.blur = prependToFunction(node.blur, function(){
return cancelBlur;
});
});
ensureEventExists(document, "uiComposeImageAdded");
$(document).on("uiComposeImageAdded", function(){
document.querySelector(".js-docked-compose .js-compose-text").focus();
});
});
//
// Block: Fix DM reply input box not getting focused after opening a conversation.
//
if (checkPropertyExists(TD, "components", "ConversationDetailView", "prototype", "showChirp")){
TD.components.ConversationDetailView.prototype.showChirp = appendToFunction(TD.components.ConversationDetailView.prototype.showChirp, function(){
return if !$TDX.focusDmInput;
setTimeout(function(){
document.querySelector(".js-reply-tweetbox").focus();
}, 100);
});
}
//
// Block: Hold Shift to restore cleared column.
//
execSafe(function supportShiftToClearColumn(){
ensurePropertyExists(TD, "vo", "Column", "prototype", "clear");
let holdingShift = false;
const updateShiftState = (pressed) => {
if (pressed != holdingShift){
holdingShift = pressed;
$("button[data-action='clear']").children("span").text(holdingShift ? "Restore" : "Clear");
}
};
const resetActiveFocus = () => {
document.activeElement.blur();
};
document.addEventListener("keydown", function(e){
if (e.shiftKey && (document.activeElement === null || !("value" in document.activeElement))){
updateShiftState(true);
}
});
document.addEventListener("keyup", function(e){
if (!e.shiftKey){
updateShiftState(false);
}
});
TD.vo.Column.prototype.clear = prependToFunction(TD.vo.Column.prototype.clear, function(){
window.setTimeout(resetActiveFocus, 0); // unfocuses the Clear button, otherwise it steals keyboard input
if (holdingShift){
this.model.setClearedTimestamp(0);
this.reloadTweets();
return true;
}
});
});
//
// Block: Make temporary search column appear as the first one and clear the input box.
//
execSafe(function setupSearchColumnHook(){
ensurePropertyExists(TD, "controller", "columnManager", "_columnOrder");
ensurePropertyExists(TD, "controller", "columnManager", "move");
$(document).on("uiSearchNoTemporaryColumn", function(e, data){
if (data.query && data.searchScope !== "users" && !data.columnKey){
if ($TDX.openSearchInFirstColumn){
const order = TD.controller.columnManager._columnOrder;
if (order.length > 1){
const columnKey = order[order.length - 1];
order.splice(order.length - 1, 1);
order.splice(1, 0, columnKey);
TD.controller.columnManager.move(columnKey, "left");
}
}
if (!("tduckResetInput" in data)){
$(".js-app-search-input").val("");
$(".js-perform-search").blur();
}
}
});
});
//
// Block: Reorder search results to move accounts above hashtags.
//
onAppReady.push(function reorderSearchResults(){
const container = $(".js-search-in-popover");
const hashtags = $$(".js-typeahead-topic-list", container);
$$(".js-typeahead-user-list", container).insertBefore(hashtags);
hashtags.addClass("list-divider");
});
//
// Block: Revert Like/Follow dialogs being closed after clicking an action.
//
execSafe(function setupLikeFollowDialogRevert(){
const prevSetTimeout = window.setTimeout;
const overrideState = function(){
return if !$TDX.keepLikeFollowDialogsOpen;
window.setTimeout = function(func, timeout){
return timeout !== 500 && prevSetTimeout.apply(this, arguments);
};
};
const restoreState = function(context, key){
window.setTimeout = prevSetTimeout;
if ($TDX.keepLikeFollowDialogsOpen && key in context.state){
context.state[key] = false;
}
};
$(document).on("uiShowFavoriteFromOptions", function(){
$(".js-btn-fav", ".js-modal-inner").each(function(){
let event = $._data(this, "events").click[0];
let handler = event.handler;
event.handler = function(){
overrideState();
handler.apply(this, arguments);
restoreState($._data(document, "events").dataFavoriteState[0].handler.context, "stopSubsequentLikes");
};
});
});
$(document).on("uiShowFollowFromOptions", function(){
$(".js-component", ".js-modal-inner").each(function(){
let event = $._data(this, "events").click[0];
let handler = event.handler;
let context = handler.context;
event.handler = function(){
overrideState();
handler.apply(this, arguments);
restoreState(context, "stopSubsequentFollows");
};
});
});
});
//
// Block: Fix broken horizontal scrolling of column container when holding Shift.
//
if (checkPropertyExists(TD, "ui", "columns", "setupColumnScrollListeners")){
TD.ui.columns.setupColumnScrollListeners = appendToFunction(TD.ui.columns.setupColumnScrollListeners, function(column){
const ele = document.querySelector(".js-column[data-column='" + column.model.getKey() + "']");
return if ele == null;
$(ele).off("onmousewheel").on("mousewheel", ".scroll-v", function(e){
e.stopImmediatePropagation();
});
window.TDGF_prioritizeNewestEvent(ele, "mousewheel");
});
}
//
// Block: Fix DM image previews and GIF thumbnails not loading due to new URLs.
//
if (checkPropertyExists(TD, "services", "TwitterMedia", "prototype", "getTwitterPreviewUrl")){
const prevFunc = TD.services.TwitterMedia.prototype.getTwitterPreviewUrl;
TD.services.TwitterMedia.prototype.getTwitterPreviewUrl = function(){
const url = prevFunc.apply(this, arguments);
if (url.startsWith("https://ton.twitter.com/1.1/ton/data/dm/") || url.startsWith("https://pbs.twimg.com/tweet_video_thumb/")){
const format = url.match(/\?.*format=(\w+)/);
if (format && format.length === 2){
const fix = `.${format[1]}?`;
if (!url.includes(fix)){
return url.replace("?", fix);
}
}
}
return url;
};
}
//
// Block: Fix DMs not being marked as read when replying to them.
//
execSafe(function markRepliedDMsAsRead(){
ensurePropertyExists(TD, "controller", "clients", "getClient");
ensurePropertyExists(TD, "services", "Conversations", "prototype", "getConversation");
$(document).on("dataDmSent", function(e, data){
const client = TD.controller.clients.getClient(data.request.accountKey);
return if !client;
const conversation = client.conversations.getConversation(data.request.conversationId);
return if !conversation;
conversation.markAsRead();
});
});
//
// Block: Limit amount of loaded DMs to avoid massive lag from re-opening them several times.
//
if (checkPropertyExists(TD, "services", "TwitterConversation", "prototype", "renderThread")){
const prevFunc = TD.services.TwitterConversation.prototype.renderThread;
TD.services.TwitterConversation.prototype.renderThread = function(){
const prevMessages = this.messages;
this.messages = prevMessages.slice(0, 100);
const result = prevFunc.apply(this, arguments);
this.messages = prevMessages;
return result;
};
}
//
// Block: Fix scheduled tweets not showing up sometimes.
//
execSafe(function fixScheduledTweets(){
ensurePropertyExists(TD, "controller", "columnManager", "getAll");
ensureEventExists(document, "dataTweetSent");
$(document).on("dataTweetSent", function(e, data){
if (data.response.state && data.response.state === "scheduled"){
const column = Object.values(TD.controller.columnManager.getAll()).find(column => column.model.state.type === "scheduled");
return if !column;
setTimeout(function(){
column.reloadTweets();
}, 1000);
}
});
});
//
// Block: Let's make retweets lowercase again.
//
window.TDGF_injectMustache("status/tweet_single.mustache", "replace", "{{_i}} Retweeted{{/i}}", "{{_i}} retweeted{{/i}}");
if (checkPropertyExists(TD, "services", "TwitterActionRetweet", "prototype", "generateText")){
TD.services.TwitterActionRetweet.prototype.generateText = appendToFunction(TD.services.TwitterActionRetweet.prototype.generateText, function(){
this.text = this.text.replace(" Retweeted", " retweeted");
this.htmlText = this.htmlText.replace(" Retweeted", " retweeted");
});
}
if (checkPropertyExists(TD, "services", "TwitterActionRetweetedInteraction", "prototype", "generateText")){
TD.services.TwitterActionRetweetedInteraction.prototype.generateText = appendToFunction(TD.services.TwitterActionRetweetedInteraction.prototype.generateText, function(){
this.htmlText = this.htmlText.replace(" Retweeted", " retweeted").replace(" Retweet", " retweet");
});
}

View File

@ -19,7 +19,9 @@
constructor(){
this.installed = [];
this.disabled = [];
this.waiting = [];
this.waitingForFeatures = [];
this.waitingForReady = [];
this.areFeaturesLoaded = false;
}
isDisabled(plugin){
@ -38,17 +40,26 @@
}
if (!this.isDisabled(plugin)){
plugin.obj.enabled();
this.runWhenFeaturesLoaded(plugin);
this.runWhenReady(plugin);
}
}
runWhenFeaturesLoaded(plugin){
if (this.areFeaturesLoaded){
plugin.obj.enabled();
}
else{
this.waitingForFeatures.push(plugin);
}
}
runWhenReady(plugin){
if (TD.ready){
plugin.obj.ready();
}
else{
this.waiting.push(plugin);
this.waitingForReady.push(plugin);
}
}
@ -82,9 +93,29 @@
}
}
onFeaturesLoaded(){
window.TDPF_getColumnName = window.TDGF_getColumnName;
window.TDPF_reloadColumns = window.TDGF_reloadColumns;
window.TDPF_prioritizeNewestEvent = window.TDGF_prioritizeNewestEvent;
window.TDPF_injectMustache = window.TDGF_injectMustache;
window.TDPF_registerPropertyUpdateCallback = window.TDGF_registerPropertyUpdateCallback;
window.TDPF_playVideo = function(urlOrObject, username){
if (typeof urlOrObject === "string"){
window.TDGF_playVideo(urlOrObject, null, username);
}
else{
window.TDGF_playVideo(urlOrObject.videoUrl, urlOrObject.tweetUrl, urlOrObject.username);
}
};
this.waitingForFeatures.forEach(plugin => plugin.obj.enabled());
this.waitingForFeatures = [];
this.areFeaturesLoaded = true;
}
onReady(){
this.waiting.forEach(plugin => plugin.obj.ready());
this.waiting = [];
this.waitingForReady.forEach(plugin => plugin.obj.ready());
this.waitingForReady = [];
}
};
@ -116,23 +147,5 @@
};
})();
//
// Block: Setup bridges to global functions.
//
window.TDPF_getColumnName = window.TDGF_getColumnName;
window.TDPF_reloadColumns = window.TDGF_reloadColumns;
window.TDPF_prioritizeNewestEvent = window.TDGF_prioritizeNewestEvent;
window.TDPF_injectMustache = window.TDGF_injectMustache;
window.TDPF_registerPropertyUpdateCallback = window.TDGF_registerPropertyUpdateCallback;
window.TDPF_playVideo = function(urlOrObject, username){
if (typeof urlOrObject === "string"){
window.TDGF_playVideo(urlOrObject, null, username);
}
else{
window.TDGF_playVideo(urlOrObject.videoUrl, urlOrObject.tweetUrl, urlOrObject.username);
}
};
#import "scripts/plugins.base.js"
})();

View File

@ -353,13 +353,7 @@
<None Include="Resources\Plugins\timeline-polls\browser.js" />
<None Include="Resources\PostBuild.fsx" />
<None Include="Resources\PostCefUpdate.ps1" />
<None Include="Resources\Scripts\code.js" />
<None Include="Resources\Scripts\imports\markup\introduction.html" />
<None Include="Resources\Scripts\imports\markup\offline.html" />
<None Include="Resources\Scripts\imports\markup\pin.html" />
<None Include="Resources\Scripts\imports\scripts\browser.features.js" />
<None Include="Resources\Scripts\imports\scripts\browser.globals.js" />
<None Include="Resources\Scripts\imports\scripts\browser.tweaks.js" />
<None Include="Resources\Scripts\imports\scripts\plugins.base.js" />
<None Include="Resources\Scripts\imports\styles\introduction.css" />
<None Include="Resources\Scripts\imports\styles\twitter.base.css" />
@ -399,6 +393,74 @@
<Name>TweetLib.Communication</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="Resources\Content\.all.js" />
<Content Include="Resources\Content\api\bridge.js" />
<Content Include="Resources\Content\api\ready.js" />
<Content Include="Resources\Content\api\jquery.js" />
<Content Include="Resources\Content\api\td.js" />
<Content Include="Resources\Content\api\utils.js" />
<Content Include="Resources\Content\bootstrap.js" />
<Content Include="Resources\Content\features\perform_search.js" />
<Content Include="Resources\Content\features\screenshot_tweet.js" />
<Content Include="Resources\Content\features\bypass_t.co_links.js" />
<Content Include="Resources\Content\features\clear_search_input.js" />
<Content Include="Resources\Content\features\configure_language_for_translations.js" />
<Content Include="Resources\Content\features\disable_clipboard_formatting.js" />
<Content Include="Resources\Content\features\disable_td_metrics.js" />
<Content Include="Resources\Content\features\drag_links_onto_columns.js" />
<Content Include="Resources\Content\features\expand_links_or_show_tooltip.js" />
<Content Include="Resources\Content\features\fix_dm_input_box_focus.js" />
<Content Include="Resources\Content\features\fix_horizontal_scrolling_of_column_container.js" />
<Content Include="Resources\Content\features\fix_marking_dm_as_read_when_replying.js" />
<Content Include="Resources\Content\features\fix_media_preview_urls.js" />
<Content Include="Resources\Content\features\fix_missing_bing_translator_languages.js" />
<Content Include="Resources\Content\features\fix_os_name.js" />
<Content Include="Resources\Content\features\fix_scheduled_tweets_not_appearing.js" />
<Content Include="Resources\Content\features\fix_youtube_previews.js" />
<Content Include="Resources\Content\features\focus_composer_after_alt_tab.js" />
<Content Include="Resources\Content\features\focus_composer_after_image_upload.js" />
<Content Include="Resources\Content\features\focus_composer_after_switching_account.js" />
<Content Include="Resources\Content\features\keep_like_follow_dialogs_open.js" />
<Content Include="Resources\Content\features\limit_loaded_dm_count.js" />
<Content Include="Resources\Content\features\middle_click_tweet_icon_actions.js" />
<Content Include="Resources\Content\features\move_accounts_above_hashtags_in_search.js" />
<Content Include="Resources\Content\features\offline_notification.js" />
<Content Include="Resources\Content\features\open_search_in_first_column.js" />
<Content Include="Resources\Content\features\paste_images_from_clipboard.js" />
<Content Include="Resources\Content\features\ready_plugins.js" />
<Content Include="Resources\Content\features\make_retweets_lowercase.js" />
<Content Include="Resources\Content\features\register_global_functions.js" />
<Content Include="Resources\Content\features\restore_cleared_column.js" />
<Content Include="Resources\Content\features\open_search_externally.js" />
<Content Include="Resources\Content\features\setup_column_type_attributes.js" />
<Content Include="Resources\Content\features\register_composer_active_event.js" />
<Content Include="Resources\Content\features\inject_css.js" />
<Content Include="Resources\Content\features\setup_desktop_notifications.js" />
<Content Include="Resources\Content\features\setup_link_context_menu.js" />
<Content Include="Resources\Content\features\setup_sound_notifications.js" />
<Content Include="Resources\Content\features\add_tweetduck_to_settings_menu.js" />
<Content Include="Resources\Content\features\hook_theme_settings.js" />
<Content Include="Resources\Content\features\setup_tweetduck_account_bamboozle.js" />
<Content Include="Resources\Content\features\setup_tweet_context_menu.js" />
<Content Include="Resources\Content\features\setup_video_player.js" />
<Content Include="Resources\Content\features\configure_first_day_of_week.js" />
<Content Include="Resources\Content\features\skip_pre_login_page.js" />
<Content Include="Resources\Content\features\handle_extra_mouse_buttons.js" />
<Content Include="Resources\Content\features\pin_composer_icon.js" />
<Content Include="Resources\Content\globals\reload_browser.js" />
<Content Include="Resources\Content\globals\apply_rot13.js" />
<Content Include="Resources\Content\globals\get_class_style_property.js" />
<Content Include="Resources\Content\globals\get_column_name.js" />
<Content Include="Resources\Content\globals\get_hovered_column.js" />
<Content Include="Resources\Content\globals\get_hovered_tweet.js" />
<Content Include="Resources\Content\globals\inject_mustache.js" />
<Content Include="Resources\Content\globals\patch_functions.js" />
<Content Include="Resources\Content\globals\prioritize_newest_event.js" />
<Content Include="Resources\Content\globals\reload_columns.js" />
<Content Include="Resources\Content\globals\show_tweet_detail.js" />
<Content Include="Resources\Scripts\bootstrap.tweetdeck.js" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PostBuildEvent>rmdir "$(ProjectDir)bin\Debug"