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:
parent
7239dcf4d2
commit
ed4f7b6b72
.idea/.idea.TweetDuck/.idea
Browser
Resources
Content
.all.js
api
bootstrap.jsfeatures
add_tweetduck_to_settings_menu.jsbypass_t.co_links.jsclear_search_input.jsconfigure_first_day_of_week.jsconfigure_language_for_translations.jsdisable_clipboard_formatting.jsdisable_td_metrics.jsdrag_links_onto_columns.jsexpand_links_or_show_tooltip.jsfix_dm_input_box_focus.jsfix_horizontal_scrolling_of_column_container.jsfix_marking_dm_as_read_when_replying.jsfix_media_preview_urls.jsfix_missing_bing_translator_languages.jsfix_os_name.jsfix_scheduled_tweets_not_appearing.jsfix_youtube_previews.jsfocus_composer_after_alt_tab.jsfocus_composer_after_image_upload.jsfocus_composer_after_switching_account.jshandle_extra_mouse_buttons.jshook_theme_settings.jsinject_css.jskeep_like_follow_dialogs_open.jslimit_loaded_dm_count.jsmake_retweets_lowercase.jsmiddle_click_tweet_icon_actions.jsmove_accounts_above_hashtags_in_search.jsoffline_notification.jsopen_search_externally.jsopen_search_in_first_column.jspaste_images_from_clipboard.jsperform_search.jspin_composer_icon.jsready_plugins.jsregister_composer_active_event.jsregister_global_functions.jsrestore_cleared_column.jsscreenshot_tweet.jssetup_column_type_attributes.jssetup_desktop_notifications.jssetup_link_context_menu.jssetup_sound_notifications.jssetup_tweet_context_menu.jssetup_tweetduck_account_bamboozle.jssetup_video_player.jsskip_pre_login_page.js
globals
Plugins/edit-design
Scripts
6
.idea/.idea.TweetDuck/.idea/jsLibraryMappings.xml
Normal file
6
.idea/.idea.TweetDuck/.idea/jsLibraryMappings.xml
Normal 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>
|
@ -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);
|
||||
}
|
||||
|
@ -368,6 +368,10 @@ protected override void WndProc(ref Message m) {
|
||||
|
||||
// bridge methods
|
||||
|
||||
public void OnFeaturesLoaded() {
|
||||
browser.OnFeaturesLoaded();
|
||||
}
|
||||
|
||||
public void PauseNotification() {
|
||||
notification.PauseNotification();
|
||||
}
|
||||
|
@ -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
74
Resources/Content/.all.js
Normal 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,
|
||||
];
|
71
Resources/Content/api/bridge.js
Normal file
71
Resources/Content/api/bridge.js
Normal 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
67
Resources/Content/api/jquery.js
vendored
Normal 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;
|
||||
}
|
41
Resources/Content/api/ready.js
Normal file
41
Resources/Content/api/ready.js
Normal 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
375
Resources/Content/api/td.js
Normal 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;
|
49
Resources/Content/api/utils.js
Normal file
49
Resources/Content/api/utils.js
Normal 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
45
Resources/Content/bootstrap.js
vendored
Normal 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).`);
|
||||
});
|
40
Resources/Content/features/add_tweetduck_to_settings_menu.js
Normal file
40
Resources/Content/features/add_tweetduck_to_settings_menu.js
Normal 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));
|
||||
});
|
||||
};
|
104
Resources/Content/features/bypass_t.co_links.js
Normal file
104
Resources/Content/features/bypass_t.co_links.js
Normal 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);
|
||||
});
|
||||
}
|
||||
};
|
28
Resources/Content/features/clear_search_input.js
Normal file
28
Resources/Content/features/clear_search_input.js
Normal 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();
|
||||
}
|
||||
});
|
||||
};
|
34
Resources/Content/features/configure_first_day_of_week.js
Normal file
34
Resources/Content/features/configure_first_day_of_week.js
Normal 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);
|
||||
});
|
||||
};
|
@ -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);
|
||||
});
|
||||
};
|
10
Resources/Content/features/disable_clipboard_formatting.js
Normal file
10
Resources/Content/features/disable_clipboard_formatting.js
Normal 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);
|
||||
});
|
||||
};
|
25
Resources/Content/features/disable_td_metrics.js
Normal file
25
Resources/Content/features/disable_td_metrics.js
Normal 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"];
|
||||
});
|
||||
};
|
80
Resources/Content/features/drag_links_onto_columns.js
Normal file
80
Resources/Content/features/drag_links_onto_columns.js
Normal 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]);
|
||||
}
|
||||
};
|
||||
};
|
66
Resources/Content/features/expand_links_or_show_tooltip.js
Normal file
66
Resources/Content/features/expand_links_or_show_tooltip.js
Normal 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;
|
||||
}
|
||||
});
|
||||
};
|
21
Resources/Content/features/fix_dm_input_box_focus.js
Normal file
21
Resources/Content/features/fix_dm_input_box_focus.js
Normal 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);
|
||||
}
|
||||
});
|
||||
};
|
@ -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");
|
||||
});
|
||||
};
|
@ -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();
|
||||
});
|
||||
};
|
36
Resources/Content/features/fix_media_preview_urls.js
Normal file
36
Resources/Content/features/fix_media_preview_urls.js
Normal 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));
|
||||
});
|
||||
};
|
@ -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;
|
||||
});
|
||||
};
|
17
Resources/Content/features/fix_os_name.js
Normal file
17
Resources/Content/features/fix_os_name.js
Normal 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";
|
||||
};
|
||||
};
|
@ -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();
|
||||
}
|
||||
});
|
||||
};
|
20
Resources/Content/features/fix_youtube_previews.js
Normal file
20
Resources/Content/features/fix_youtube_previews.js
Normal 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;
|
||||
};
|
32
Resources/Content/features/focus_composer_after_alt_tab.js
Normal file
32
Resources/Content/features/focus_composer_after_alt_tab.js
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
@ -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();
|
||||
});
|
||||
});
|
||||
};
|
@ -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);
|
||||
});
|
||||
});
|
||||
};
|
49
Resources/Content/features/handle_extra_mouse_buttons.js
Normal file
49
Resources/Content/features/handle_extra_mouse_buttons.js
Normal 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();
|
||||
}
|
||||
};
|
||||
};
|
48
Resources/Content/features/hook_theme_settings.js
Normal file
48
Resources/Content/features/hook_theme_settings.js
Normal 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);
|
||||
}
|
31
Resources/Content/features/inject_css.js
Normal file
31
Resources/Content/features/inject_css.js
Normal 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);
|
||||
}
|
||||
};
|
||||
};
|
54
Resources/Content/features/keep_like_follow_dialogs_open.js
Normal file
54
Resources/Content/features/keep_like_follow_dialogs_open.js
Normal 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");
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
22
Resources/Content/features/limit_loaded_dm_count.js
Normal file
22
Resources/Content/features/limit_loaded_dm_count.js
Normal 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;
|
||||
});
|
||||
};
|
24
Resources/Content/features/make_retweets_lowercase.js
Normal file
24
Resources/Content/features/make_retweets_lowercase.js
Normal 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");
|
||||
});
|
||||
}
|
||||
};
|
@ -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();
|
||||
});
|
||||
};
|
@ -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");
|
||||
});
|
||||
};
|
63
Resources/Content/features/offline_notification.js
Normal file
63
Resources/Content/features/offline_notification.js
Normal 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);
|
||||
};
|
34
Resources/Content/features/open_search_externally.js
Normal file
34
Resources/Content/features/open_search_externally.js
Normal 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, $());
|
||||
});
|
||||
});
|
||||
};
|
36
Resources/Content/features/open_search_in_first_column.js
Normal file
36
Resources/Content/features/open_search_in_first_column.js
Normal 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");
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
45
Resources/Content/features/paste_images_from_clipboard.js
Normal file
45
Resources/Content/features/paste_images_from_clipboard.js
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
47
Resources/Content/features/perform_search.js
Normal file
47
Resources/Content/features/perform_search.js
Normal 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.");
|
||||
};
|
||||
});
|
||||
};
|
41
Resources/Content/features/pin_composer_icon.js
Normal file
41
Resources/Content/features/pin_composer_icon.js
Normal 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)";
|
||||
}
|
||||
});
|
||||
};
|
10
Resources/Content/features/ready_plugins.js
Normal file
10
Resources/Content/features/ready_plugins.js
Normal 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());
|
||||
};
|
16
Resources/Content/features/register_composer_active_event.js
Normal file
16
Resources/Content/features/register_composer_active_event.js
Normal 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);
|
||||
});
|
||||
};
|
20
Resources/Content/features/register_global_functions.js
Normal file
20
Resources/Content/features/register_global_functions.js
Normal 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;
|
||||
};
|
46
Resources/Content/features/restore_cleared_column.js
Normal file
46
Resources/Content/features/restore_cleared_column.js
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
65
Resources/Content/features/screenshot_tweet.js
Normal file
65
Resources/Content/features/screenshot_tweet.js
Normal 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);
|
||||
};
|
||||
};
|
25
Resources/Content/features/setup_column_type_attributes.js
Normal file
25
Resources/Content/features/setup_column_type_attributes.js
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
243
Resources/Content/features/setup_desktop_notifications.js
Normal file
243
Resources/Content/features/setup_desktop_notifications.js
Normal 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);
|
||||
});
|
||||
}
|
||||
};
|
42
Resources/Content/features/setup_link_context_menu.js
Normal file
42
Resources/Content/features/setup_link_context_menu.js
Normal 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 });
|
||||
};
|
42
Resources/Content/features/setup_sound_notifications.js
Normal file
42
Resources/Content/features/setup_sound_notifications.js
Normal 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);
|
||||
}
|
||||
});
|
||||
};
|
46
Resources/Content/features/setup_tweet_context_menu.js
Normal file
46
Resources/Content/features/setup_tweet_context_menu.js
Normal 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 });
|
||||
};
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
147
Resources/Content/features/setup_video_player.js
Normal file
147
Resources/Content/features/setup_video_player.js
Normal 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;
|
||||
}
|
||||
};
|
14
Resources/Content/features/skip_pre_login_page.js
Normal file
14
Resources/Content/features/skip_pre_login_page.js
Normal 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";
|
||||
});
|
||||
};
|
24
Resources/Content/globals/apply_rot13.js
Normal file
24
Resources/Content/globals/apply_rot13.js
Normal 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);
|
||||
}));
|
||||
}
|
17
Resources/Content/globals/get_class_style_property.js
Normal file
17
Resources/Content/globals/get_class_style_property.js
Normal 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;
|
||||
}
|
25
Resources/Content/globals/get_column_name.js
Normal file
25
Resources/Content/globals/get_column_name.js
Normal 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] || "";
|
||||
}
|
23
Resources/Content/globals/get_hovered_column.js
Normal file
23
Resources/Content/globals/get_hovered_column.js
Normal 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;
|
||||
}
|
28
Resources/Content/globals/get_hovered_tweet.js
Normal file
28
Resources/Content/globals/get_hovered_tweet.js
Normal 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;
|
||||
}
|
47
Resources/Content/globals/inject_mustache.js
Normal file
47
Resources/Content/globals/inject_mustache.js
Normal 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;
|
||||
}
|
48
Resources/Content/globals/patch_functions.js
Normal file
48
Resources/Content/globals/patch_functions.js
Normal 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;
|
||||
});
|
||||
}
|
19
Resources/Content/globals/prioritize_newest_event.js
Normal file
19
Resources/Content/globals/prioritize_newest_event.js
Normal 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;
|
||||
}
|
17
Resources/Content/globals/reload_browser.js
Normal file
17
Resources/Content/globals/reload_browser.js
Normal 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();
|
||||
}
|
12
Resources/Content/globals/reload_columns.js
Normal file
12
Resources/Content/globals/reload_columns.js
Normal 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;
|
68
Resources/Content/globals/show_tweet_detail.js
Normal file
68
Resources/Content/globals/show_tweet_detail.js
Normal 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.");
|
||||
};
|
@ -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
|
||||
|
62
Resources/Scripts/bootstrap.tweetdeck.js
vendored
Normal file
62
Resources/Scripts/bootstrap.tweetdeck.js
vendored
Normal 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);
|
||||
})();
|
@ -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 || {});
|
@ -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>
|
@ -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 |
@ -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);
|
||||
})();
|
@ -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
|
||||
};
|
@ -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");
|
||||
});
|
||||
}
|
@ -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"
|
||||
})();
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user