mirror of
https://github.com/chylex/TweetDuck.git
synced 2025-05-14 11:34:08 +02:00
466 lines
13 KiB
JavaScript
466 lines
13 KiB
JavaScript
(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'],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 || {});
|