diff --git a/.eslintrc.json b/.eslintrc.json index 92c3ac7..940e343 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -32,6 +32,7 @@ "no-trailing-spaces":"error", "prefer-const": "warn", "one-var": ["warn", "never"], - "no-multiple-empty-lines": ["warn", { "max": 1 }] + "no-multiple-empty-lines": ["warn", { "max": 1 }], + "arrow-parens": ["warn", "as-needed"] } -} \ No newline at end of file +} diff --git a/README.md b/README.md index 5f111d9..f1ffbc9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Join the chat at https://gitter.im/soscripted/sox](https://badges.gitter.im/soscripted/sox.svg)](https://gitter.im/soscripted/sox?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -### SOX v2.4.0 +### SOX v2.5.0 Stack Overflow Extras (*SOX*) is a project that stemmed from the [Stack Overflow Optional Features (SOOF)](https://github.com/shu8/Stack-Overflow-Optional-Features) project. @@ -10,10 +10,19 @@ Note: This project has no relation to Stack Overflow or Stack Exchange; it is si ## Installation & Requirements -1. Install [Tampermonkey](http://tampermonkey.net/) (for Chrome or Firefox). These are userscript managers that *must* be installed in order for this to work, as the script relies on certain `GM_*` functions in order to save your settings! Tampermonkey is available for many more browsers, and whilst we do not explicitly support them, SOX should work on them. **Note: Greasemonkey 4 and upwards [is not supported with SOX](https://github.com/soscripted/sox/issues/306).** -2. Install the script. Clicking on 'install' below will make your userscript manager prompt you automatically to install it. +1. Install a userscript manager; these are free extensions available for all popular browsers that allow you to manage and install userscripts, along with exposing certain code functions that SOX requires. - - Official Version: [install](https://github.com/soscripted/sox/raw/v2.4.0/sox.user.js). [view source](https://github.com/soscripted/sox/blob/v2.4.0/sox.user.js) + We recommend [Tampermonkey](http://tampermonkey.net/) for Chrome and Firefox. + + Whilst SOX only explicitly supports Chrome and Firefox, it should work on any popular browser that can run userscripts. + + **Note: Greasemonkey 4 and upwards [is not supported with SOX](https://github.com/soscripted/sox/issues/306).** + + **There seems to be [an issue with Tampermonkey on Firefox](https://github.com/Tampermonkey/tampermonkey/issues/477) where userscripts don't seem to run. If this happens, please restart your browser and/or computer before raising an issue on GitHub, as a restart seems to fix this!** + +2. Install the script. Clicking on 'install' below will make Tampermonkey prompt you automatically to install it. + + - Official Version: [install](https://github.com/soscripted/sox/raw/v2.5.0/sox.user.js). [view source](https://github.com/soscripted/sox/blob/v2.5.0/sox.user.js) - Development Version: [install](https://github.com/soscripted/sox/raw/dev/sox.user.js). [view source](https://github.com/soscripted/sox/blob/dev/sox.user.js) 3. Go to any site in the Stack Exchange Network (e.g. [Super User](http://superuser.com/) or [Stack Overflow](http://stackoverflow.com/)). You will automatically be asked to choose and save your settings. A toggle button (gears icon) will be added to your topbar where you can change these later on: diff --git a/docs/index.html b/docs/index.html index ea549ba..14bd0d4 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,10 +1,10 @@ - + - sox by soscripted + SOX by soscripted @@ -16,7 +16,7 @@
-

sox

+

SOX

A userscript for the Stack Exchange websites to add a bunch of optional user-selectable features

View the Project on GitHub

@@ -46,43 +46,43 @@

  1. Greasemonkey (for Firefox), Tampermonkey (for Chrome), or NinjaKit for Safari. These are userscript managers that must be installed in order for this to work, as the script relies on certain GM_* functions in order to save your settings!
  2. -

    Install the script. Clicking on ‘install’ below will make your userscript manager prompt you automatically to install it.

    +

    Install the script. Clicking on the 'install' button below will make your userscript manager prompt you automatically to install it.

      -
    • Official Version: [install](https://github.com/soscripted/sox/raw/v2.0.1/sox.user.js). [view source](https://github.com/soscripted/sox/blob/v2.0.1/sox.user.js)
    • -
    • Development Version: [install](https://github.com/soscripted/sox/raw/dev/sox.user.js). [view source](https://github.com/soscripted/sox/blob/dev/sox.user.js)
    • +
    • Official Version: install. view source
    • +
    • Development Version: install. view source
  3. -
  4. Go to any site in the Stack Exchange Network (eg. Super User or Stack Overflow). You will automatically be asked to choose and save your settings. A toggle button +
  5. Go to any site in the Stack Exchange Network (e.g. Super User or Stack Overflow). You will automatically be asked to choose and save your settings. A toggle button (gears icon) will be added to your topbar where you can change these later on:

newdialog

-

##What features are included?

+

What features are included?

A full list of all the features is available on the SOX wiki page here.

-

##Bugs and Feature Requests

+

Bugs and Feature Requests

Please post bugs and feature requests as issues on Github, where we can track them easily and push updates quickly. Please do not post them as answers on Stack Apps – they are much harder to manage!

-

##Changes

+

Changes

Please see the change log at Stack Apps.

diff --git a/sox.common.js b/sox.common.js index 1a4ef7b..7818983 100644 --- a/sox.common.js +++ b/sox.common.js @@ -17,31 +17,31 @@ sox.debug = function() { if (!sox.info.debugging) return; for (let arg = 0; arg < arguments.length; ++arg) { - console.debug('SOX: ', arguments[arg]); + console.debug('SOX:', arguments[arg]); } }; sox.log = function() { for (let arg = 0; arg < arguments.length; ++arg) { - console.log('SOX: ', arguments[arg]); + console.log('SOX:', arguments[arg]); } }; sox.warn = function() { for (let arg = 0; arg < arguments.length; ++arg) { - console.warn('SOX: ', arguments[arg]); + console.warn('SOX:', arguments[arg]); } }; sox.error = function() { for (let arg = 0; arg < arguments.length; ++arg) { - console.error('SOX: ', arguments[arg]); + console.error('SOX:', arguments[arg]); } }; sox.loginfo = function() { for (let arg = 0; arg < arguments.length; ++arg) { - console.info('SOX: ', arguments[arg]); + console.info('SOX:', arguments[arg]); } }; @@ -49,9 +49,7 @@ let Stack; if (location.href.indexOf('github.com') === -1) { //need this so it works on FF -- CSP blocks window.eval() it seems Chat = (typeof window.CHAT === 'undefined' ? window.eval('typeof CHAT != \'undefined\' ? CHAT : undefined') : CHAT); - sox.debug('CHAT', Chat); Stack = (typeof Chat === 'undefined' ? (typeof StackExchange === 'undefined' ? window.eval('if (typeof StackExchange != "undefined") StackExchange') : (StackExchange || window.StackExchange)) : undefined); - sox.debug('Stack', Stack); } sox.Stack = Stack; @@ -104,7 +102,6 @@ } }, get accessToken() { - sox.debug('SOX Access Token: ' + (GM_getValue('SOX-accessToken', false) === false ? 'NOT SET' : 'SET')); const accessToken = GM_getValue('SOX-accessToken', false); return (accessToken == -2 ? false : accessToken); //if the user was already asked once, the value is set to -2, so make sure this is returned as false }, @@ -138,16 +135,107 @@ } sox.helpers = { - getFromAPI: function(type, id, sitename, filter, callback, sortby) { - sox.debug('Getting From API with URL: https://api.stackexchange.com/2.2/' + type + '/' + id + '?order=desc&sort=' + (sortby || 'creation') + '&site=' + sitename + '&key=' + sox.info.apikey + '&access_token=' + sox.settings.accessToken); + getFromAPI: function (details, callback) { + let { + ids, + useCache = true, + } = details; + + const { + endpoint, + childEndpoint, + sort = 'creation', + order = 'desc', + sitename, + filter, + limit, + featureId, + cacheDuration = 3, // Minutes to cache data for + } = details; + const baseURL = 'https://api.stackexchange.com/2.2/'; + const queryParams = []; + + // Cache can only be used if the featureId and IDs (as an array) have been provided + useCache = featureId && useCache && Array.isArray(ids); + const apiCache = JSON.parse(GM_getValue('SOX-apiCache', '{}')); + + if (!(featureId in apiCache)) apiCache[featureId] = {}; + const featureCache = apiCache[featureId]; + + if (!(endpoint in featureCache)) featureCache[endpoint] = []; + const endpointCache = featureCache[endpoint]; + + const endpointToIdFieldNames = { + 'questions': 'question_id', + 'answers': 'answer_id', + 'users': 'user_id', + 'comments': 'comment_id', + }; + + if (filter) queryParams.push(`filter=${filter}`); + if (order) queryParams.push(`order=${order}`); + if (limit) queryParams.push(`pagesize=${limit}`); + queryParams.push(`sort=${sort}`); + queryParams.push(`site=${sitename}`); + queryParams.push(`key=${sox.info.apikey}`); + queryParams.push(`access_token=${sox.settings.accessToken}`); + const queryString = queryParams.join('&'); + + let finalItems = []; + if (useCache) { + // Count backwards so splicing doesn't change indices + for (let i = ids.length; i >= 0; i--) { + const cachedItemIndex = endpointCache.findIndex(item => { + const idFieldName = endpointToIdFieldNames[endpoint]; + return item[idFieldName] === +ids[i]; + }); + + // Cache results for max. cacheDuraction minutes (convert to milliseconds) + const earliestRequestTime = new Date().getTime() - (60 * cacheDuration * 1000); + if (cachedItemIndex !== -1) { + const cachedItem = endpointCache[cachedItemIndex]; + if (cachedItem.sox_request_time >= earliestRequestTime) { + // If we have a cached item for this ID, delete it from `ids` so we don't request the API for it + sox.debug(`API: [${featureId}:/${endpoint}/${ids[i]}] Using cached API item`); + finalItems.push(cachedItem); + ids.splice(i, 1); + } else { + // The cached item is now stale (too old); delete it + sox.debug(`API: [${featureId}:/${endpoint}/${ids[i]}] Deleting stale cached item`); + endpointCache.splice(cachedItemIndex, 1); + } + } + } + } + + // IDs are optional for endpoints like /questions + if (ids && Array.isArray(ids)) { + if (ids.length) { + ids = ids.join(';'); + } else if (useCache) { + // The cache had details for all IDs; no need to request API at all + sox.debug(`API: [${featureId}:/${endpoint}] API Cache had details for all requested IDs, skipping API request`); + GM_setValue('SOX-apiCache', JSON.stringify(apiCache)); + sox.debug('API: Saving new cache', apiCache); + callback(finalItems); + return; + } + } - const filterQuery = filter ? '&filter=' + filter : ''; - // optional for queries like /questions - const idPath = id ? '/' + id : ''; + const idPath = ids ? `/${ids}` : ''; + let queryURL; + if (childEndpoint) { + // e.g. /posts/{ids}/revisions + queryURL = `${baseURL}${endpoint}${idPath}/${childEndpoint}?${queryString}`; + } else { + // e.g. /questions/{ids} + queryURL = `${baseURL}${endpoint}${idPath}?${queryString}`; + } + sox.debug(`API: Sending request to URL: '${queryURL}'`); $.ajax({ type: 'get', - url: 'https://api.stackexchange.com/2.2/' + type + idPath + '?order=desc&sort=' + (sortby || 'creation') + '&site=' + sitename + '&key=' + sox.info.apikey + '&access_token=' + sox.settings.accessToken + filterQuery, + url: queryURL, success: function(d) { if (d.backoff) { sox.error('SOX Error: BACKOFF: ' + d.backoff); @@ -158,7 +246,18 @@ window.open('https://stackexchange.com/oauth/dialog?client_id=7138&scope=no_expiry&redirect_uri=http://soscripted.github.io/sox/'); alert('Your access token is no longer valid. A window has been opened to request a new one.'); } else { - callback(d); + if (useCache) { + d.items.forEach(item => { + item.sox_request_time = new Date().getTime(); + finalItems.push(item); + endpointCache.push(item); + }); + GM_setValue('SOX-apiCache', JSON.stringify(apiCache)); + sox.debug('API: saving new cache', apiCache); + } else { + finalItems = d.items; + } + callback(finalItems); } }, error: function(a, b, c) { @@ -166,9 +265,11 @@ }, }); }, - observe: function(elements, callback, toObserve) { - sox.debug('observe: ' + elements); - const observer = new MutationObserver(throttle((mutations) => { + observe: function (targets, elements, callback) { + sox.debug(`OBSERVE: '${elements}' on target(s)`, targets); + if (!targets || (Array.isArray(targets) && !targets.length)) return; + + const observer = new MutationObserver(throttle(mutations => { for (let i = 0; i < mutations.length; i++) { const mutation = mutations[i]; const target = mutation.target; @@ -177,8 +278,8 @@ if (addedNodes) { for (let n = 0; n < addedNodes.length; n++) { if ($(addedNodes[n]).find(elements).length) { - callback(target); sox.debug('fire: node: ', addedNodes[n]); + callback(target); return; } } @@ -190,11 +291,14 @@ return; } } - }, 250)); + }, 1500)); + + if (Array.isArray(targets)) { + for (let i = 0; i < targets.length; i++) { + const target = targets[i]; + if (!target) continue; - if (toObserve) { - for (let i = 0; i < toObserve.length; i++) { //could be multiple elements with querySelectorAll - observer.observe(toObserve[i], { + observer.observe(target, { attributes: true, childList: true, characterData: true, @@ -202,7 +306,7 @@ }); } } else { - observer.observe(document.body, { + observer.observe(targets, { attributes: true, childList: true, characterData: true, @@ -247,7 +351,7 @@ // answer ID, question ID, user ID, comment ID ("posts/comments/ID" NOT "comment1545_5566") getIDFromLink: function(link) { // test cases: https://regex101.com/r/6P9sDX/2 - const idMatch = link.match(/\/(\d+)($|\/|\?)/); + const idMatch = link.match(/\/(\d+)/); return idMatch ? +idMatch[1] : null; }, getSiteNameFromLink: function(link) { @@ -426,4 +530,4 @@ }, }; -})(window.sox = window.sox || {}, jQuery); \ No newline at end of file +})(window.sox = window.sox || {}, jQuery); diff --git a/sox.css b/sox.css index f6275ec..c76eb69 100644 --- a/sox.css +++ b/sox.css @@ -433,6 +433,7 @@ width: 220px; margin: 6px 10px 10px 0; padding: 0px; + margin: auto; } #sox-flagPercentProgressBar:after { @@ -443,6 +444,7 @@ #sox-flagPercentHelpful { margin-bottom: 5px; + text-align: center; } /*titleEditDiff -- for the toggle button*/ @@ -490,12 +492,6 @@ padding: 1px 5px !important; } -/*hotNetworkQuestionsFiltering -- for the questions to hide*/ - -.sox-hot-network-question-filter-hide { - display: none !important; -} - /*addAuthorNameToInboxNotifications -- for the author span*/ .sox-notification-author { @@ -574,4 +570,10 @@ .sox-customMagicLinks-settings-table th { font-weight: bold; -} \ No newline at end of file +} + +/* linkedToFrom -- for the >/< chevrons */ +.sox-linkedToFrom-chevron { + color: black; + margin-left: 5px; +} diff --git a/sox.dialog.js b/sox.dialog.js index 364c3f3..33ae34c 100644 --- a/sox.dialog.js +++ b/sox.dialog.js @@ -25,11 +25,13 @@ const $exportSettingsButton = $soxSettingsDialog.find('#sox-settings-export'); const $featurePackButtons = $soxSettingsDialog.find('.sox-settings-dialog-feature-pack'); - //array of HTML strings that will be displayed as `li` items if the user has installed a new version. - - const changes = ['Introduced \'feature packs\' -- easily find and enable features we would categorise as \'major UI tweaks\', \'key features\', or \'power user fetures\'!', - 'You will no longer be forced to get an access token. If you choose not to, SOX will simply disable features that need one. Thanks @Izzy for the suggestion!', - 'Deprecated paste images feature -- it has been implemented natively by SE now!']; + // Array of HTML strings that will be displayed as `li` items if the user has installed a new version. + const changes = [ + 'Only inject into Github issues if you are on the SOX repo', + 'Fix bugs in various features', + 'Improve SOX\'s performance by making lots of behind-the-scenes improvements', + 'Deprecated the \'Hide HNQ\'s\' feature; it is now implemented natively!', + ]; function addCategory(name) { const $div = $('
', { @@ -301,7 +303,7 @@ }); //close dialog if clicked outside it - $(document).click((e) => { //close dialog if clicked outside it + $(document).click(e => { //close dialog if clicked outside it const $target = $(e.target); const isToggle = $target.is('#soxSettingsButton, #sox-settings-dialog'); const isChild = $target.parents('#soxSettingsButton, #sox-settings-dialog').is('#soxSettingsButton, #sox-settings-dialog'); @@ -362,4 +364,4 @@ }, }; -})(window.sox = window.sox || {}, jQuery); \ No newline at end of file +})(window.sox = window.sox || {}, jQuery); diff --git a/sox.features.info.json b/sox.features.info.json index 032acd1..85a0141 100644 --- a/sox.features.info.json +++ b/sox.features.info.json @@ -35,7 +35,7 @@ "extended_description": "Highlight the username of a commenter if they have posted an answer on that page.", "meta": "http://meta.stackexchange.com/questions/19574/highlight-comments-from-answer-author-in-addition-to-question-author", "match": "*://*/questions*", - "exclude": "SE1.0", + "exclude": "SE1.0,*://*/questions/ask", "feature_packs": ["major_ui", "key_feature", "power_user"] }, { "name": "displayName", @@ -48,22 +48,22 @@ "desc": "Make bounty box draggable", "meta": "http://meta.stackexchange.com/questions/170125/make-bounty-custom-message-dialog-box-draggable", "match": "*://*/questions*", - "exclude": "SE1.0" + "exclude": "SE1.0,*://*/questions/ask" }, { "name": "highlightQuestions", "desc": "Change highlighting for questions with favourite tags", "extended_description": "Changes the favourite tag question highlighting to be a more subtle, coloured left-border", "meta": "http://meta.stackexchange.com/questions/238591/should-favorite-tag-highlighting-in-question-lists-be-changed", "match": "", - "exclude": "*://chat.*.com/*", + "exclude": "*://chat.*.com/*,*://*/questions/*", "feature_packs": ["major_ui", "key_feature", "power_user"] }, { "name": "isQuestionHot", "desc": "Add a label on questions which are hot-network questions", "extended_description": "If the question you are currently viewing is HOT, a flame icon is added next to the title", "meta": "http://meta.stackexchange.com/questions/245390/let-mods-and-10k-know-when-questions-go-hot", - "match": "", - "exclude": "SE1.0" + "match": "*://*/questions/*", + "exclude": "SE1.0,*://*/questions/ask" }, { "name": "localTimestamps", "desc": "Convert timestamps to your local time", @@ -81,7 +81,7 @@ "desc": "Add the SO logo after employee names to make them stand out", "meta": "http://meta.stackexchange.com/questions/246678/should-se-staff-have-a-special-character-in-their-user-name", "match": "", - "exclude": "*://chat.*.com/*,SE1.0", + "exclude": "*://chat.*.com/*,SE1.0,*://*/questions/ask", "feature_packs": ["major_ui", "key_feature", "power_user"], "usesApi": true }, { @@ -117,7 +117,7 @@ "extended_description": "Adds a coloured box at the end of a title (that replaces the standard [duplicate], etc.) in question lists to more easily tell what the state of a question is", "meta": "http://meta.stackexchange.com/questions/257021/proposal-to-make-duplicate-closed-and-migrated-in-the-title-more-obvious", "match": "", - "exclude": "*://chat.*.com/*,SE1.0", + "exclude": "*://chat.*.com/*,SE1.0,*://*/questions/*,*://*/review*", "feature_packs": ["major_ui, key_feature", "power_user"], "usesApi": true }, { @@ -132,7 +132,7 @@ "desc": "Improve answer visibility by listing top answers", "meta": "", "match": "*://*/questions*", - "exclude": "SE1.0" + "exclude": "SE1.0,*://*/questions/ask" }, { "name": "unspoil", "desc": "Add a link to the bottom of a post to reveal all spoilers in a post", @@ -144,7 +144,7 @@ "desc": "Add timeline and revision links to the bottom of each post for quick access to them", "meta": "", "match": "*://*/questions*,*://*/review*", - "exclude": "", + "exclude": "*://*/questions/ask", "feature_packs": ["power_user"] }, { "name": "showTagWikiLinkOnTagPopup", @@ -158,19 +158,13 @@ "desc": "Hide the 'welcome back...don't forget to vote' message when visiting a site after a while", "meta": "", "match": "*://*/questions*", - "exclude": "" + "exclude": "*://*/questions/ask" }, { "name": "hideHowToAskWhenZoomed", "desc": "Hide the 'How to ask/format/tag' yellow boxes that appear when asking a question whilst zoomed in", "meta": "", "match": "*://*/questions/ask", "exclude": "" - }, { - "name": "showTagWikiLinkOnTagPopup", - "desc": "Add a link to the tag wiki page on the popup that appears when hovering over a tag", - "meta": "", - "match": "*://*/questions*,*://*/review*", - "exclude": "" }], "Comments": [{ "name": "autoShowCommentImages", @@ -178,21 +172,21 @@ "extended_description": "This feature will automatically detect comments with links to imgur and will display them inline", "meta": "", "match": "*://*/questions*,*://*/review*", - "exclude": "SE1.0", + "exclude": "SE1.0,*://*/questions/ask", "feature_packs": ["major_ui, key_feature", "power_user"] }, { "name": "commentReplies", "desc": "Add reply links to comments for quick replying (without having to type someone's username)", "meta": "http://meta.stackexchange.com/questions/74778/add-reply-link-to-comment-that-pre-populates-comment-box-with-username", "match": "*://*/questions*,*://*/review*", - "exclude": "SE1.0", + "exclude": "SE1.0,*://*/questions/ask", "feature_packs": ["power_user"] }, { "name": "commentShortcuts", "desc": "Use Ctrl+I,B,K (to italicise, bolden and add code backticks) in comments", "meta": "http://meta.stackexchange.com/questions/14756/formatting-keyboard-shortcuts-for-comments", "match": "*://*/questions*", - "exclude": "SE1.0" + "exclude": "SE1.0,*://*/questions/ask" }, { "name": "confirmNavigateAway", "desc": "Add a confirmation dialog when navigating away on pages whilst still typing a comment", @@ -231,14 +225,14 @@ "desc": "Only show the comment actions (flag/upvote) when hovering over a comment", "meta": "https://meta.stackexchange.com/q/312794", "match": "*://*/questions*,*://*/review*", - "exclude": "" + "exclude": "*://*/questions/ask" }], "Editing": [{ "name": "addSBSBtn", "desc": "Add a button to the editor toolbar to start side-by-side editing", "extended_description": "An 'SBS' button is added to the right of the markdown editor toolbar. Clicking it will make the markdown and the preview appear side-by-side", "meta": "http://meta.stackexchange.com/questions/253112/the-discourse-layout-for-side-by-side-markdown-preview", - "match": "", + "match": "*://*/questions/*", "exclude": "*://chat.*.com/*,SE1.0", "settings": [{ "id": "sbsByDefault", @@ -251,8 +245,8 @@ "desc": "Pre-defined edit comment options (checkboxes)", "extended_description": "Adds checkboxes to add canned messages for edit revisions when editing. You can make *your own* canned responses by clicking the 'Edit Reasons' button that is added to the Help dropdown menu in the topbar", "meta": "http://meta.stackexchange.com/questions/190461/improve-the-editing-flow-with-predefined-options-for-edit-summary", - "match": "", - "exclude": "*://chat.*.com/*,SE1.0", + "match": "*://*/questions/*", + "exclude": "*://chat.*.com/*,SE1.0,*://*/questions/ask", "feature_packs": ["power_user"] }, { "name": "editReasonTooltip", @@ -260,7 +254,7 @@ "extended_description": "When a post is edited, the editor is displayed underneath the post. This feature will show what the last revision's comment was as a tooltip when you hover over 'edited' underneath a post in 'edited [date] at [time]'", "meta": "http://meta.stackexchange.com/questions/2315/show-reason-for-edit-without-clicking-through-to-diff", "match": "*://*/questions*,*://*/review*", - "exclude": "SE1.0", + "exclude": "SE1.0,*://*/questions/ask", "feature_packs": ["power_user"] }, { "name": "findAndReplace", @@ -288,26 +282,26 @@ "extended_description": "Enables the inline editor on all sites, even if you don't have 2k rep there yet. Note: this feature may not work on Firefox.", "meta": "", "match": "*://*/questions*", - "exclude": "" + "exclude": "*://*/questions/ask" }], "Flags": [{ "name": "flagOutcomeTime", "desc": "Show the flag outcome time when viewing your Flag History", "meta": "", - "match": "", + "match": "*://*/users/*", "exclude": "*://chat.*.com/*,SE1.0" }, { "name": "flagPercentages", "desc": "Show flagging percentages for each type in the Flag Summary", "meta": "", - "match": "", + "match": "*://*/users/*", "exclude": "*://chat.*.com/*,SE1.0", "feature_packs": ["power_user"] }, { "name": "flagPercentageBar", "desc": "Show the total percentage of helpful flags as a coloured bar on the Flag Summary Page", "meta": "http://meta.stackoverflow.com/questions/310881/overall-percentage-of-helpful-flags", - "match": "", + "match": "*://*/users/*", "exclude": "*://chat.*.com/*,SE1.0", "feature_packs": ["power_user"] }], @@ -340,13 +334,7 @@ "desc": "Hide the 'Love this site?' module", "meta": "", "match": "", - "exclude": "*://chat.*.com/*" - }, { - "name": "hideHotNetworkQuestions", - "desc": "Hide the Hot Network Questions module", - "meta": "", - "match": "", - "exclude": "*://chat.*.com/*" + "exclude": "*://chat.*.com/*,*://*/review*" }, { "name": "linkedToFrom", "desc": "Add an arrow to linked posts in the sidebar to show whether they are linked to or linked from", @@ -407,23 +395,16 @@ "desc": "Make vote buttons next to posts sticky whilst scrolling on that post", "meta": "http://meta.stackexchange.com/a/35047/260841", "match": "*://*/questions*,*://*/review*", - "exclude": "SE1.0", + "exclude": "SE1.0,*://*/questions/ask", "feature_packs": ["major_ui, key_feature", "power_user"] }, { "name": "disableVoteButtons", "desc": "Disable vote buttons on your own posts and deleted posts, which you cannot vote on", "meta": "", "match": "*://*/questions*", - "exclude": "SE1.0" + "exclude": "SE1.0,*://*/questions/ask" }], "Extras": [{ - "name": "alwaysShowImageUploadLinkBox", - "desc": "Always show the 'Link from the web' box when uploading an image", - "extended_description": "Removes the 'you can also provide a link from the web' button when uploading images, and shows the URL input field by default", - "meta": "http://meta.stackoverflow.com/q/306888/3541881", - "match": "*://*/questions*,*://*/review*", - "exclude": "SE1.0" - }, { "name": "linkedPostsInline", "desc": "Display linked posts inline by clicking on an arrow", "extended_description": "Adds a button next to links to posts on the same site that expand to show the post inline", @@ -443,7 +424,7 @@ "desc": "Show when the post's author was last seen and whether they are registered", "meta": "", "match": "*://*/questions*,*://*/review*", - "exclude": "SE1.0", + "exclude": "SE1.0,*://*/questions/ask", "feature_packs": ["power_user"], "usesApi": true }, { @@ -452,7 +433,7 @@ "extended_description": "When you click 'share' under a post, this displays a referral link, which incorporates your user ID. This option strips your user ID from the displayed link, preventing inadvertent privacy leaks when disseminating the link via copy-paste. The social-media sharing buttons are unaffected, since the lack of privacy is obvious.", "meta": "https://meta.stackexchange.com/questions/74274/privacy-leak-in-permalink", "match": "*://*/questions*,*://*/review*", - "exclude": "SE1.0", + "exclude": "SE1.0,*://*/questions/ask", "feature_packs": ["power_user"] }, { "name": "shareLinksMarkdown", @@ -460,13 +441,13 @@ "extended_description": "When you click 'share' under a post, this will convert the URL given to a markdown-friendly version, with the post name as the link text. This feature also automatically copies the converted string to your clipboard", "meta": "http://meta.stackexchange.com/questions/126544/add-a-second-share-button-to-posts-with-comment-ready-links", "match": "*://*/questions*,*://*/review*", - "exclude": "SE1.0" + "exclude": "SE1.0,*://*/questions/ask" }, { "name": "sortByBountyAmount", "desc": "Add an option to filter bounties by their amount", "meta": "http://meta.stackexchange.com/questions/7753/please-give-us-the-ability-to-sort-featured-tab-by-bounty-amount", "match": "", - "exclude": "*://*/users/*,*://chat.*.com/*,SE1.0" + "exclude": "*://*/users/*,*://chat.*.com/*,SE1.0,*://*/questions/*,*://*/review*" }, { "name": "warnNotLoggedIn", "desc": "Warn you when you are not logged in", @@ -550,7 +531,7 @@ "extended_description": "Allows you to create your own magic links ('[text]' auto-converts to a link of your choice). Links can use the placeholders $BASEURL, $METABASEURL, $QUESTIONID, and $ANSWERID which are auto-replaced with the respective data. You can set the reasons by clicking the 'Magic Links' button added to the 'Help' dropdown at the top-right of the page. See https://stackapps.com/q/6650 for more details.", "meta": "", "match": "", - "exclude": "*://chat.*.com/*,SE1.0", + "exclude": "*://chat.*.com/*,SE1.0,*://*/questions/ask", "feature_packs": ["power_user"] }] } diff --git a/sox.features.js b/sox.features.js index a349616..b5396ee 100644 --- a/sox.features.js +++ b/sox.features.js @@ -12,13 +12,13 @@ dragBounty: function() { // Description: Makes the bounty window draggable - sox.helpers.observe('#start-bounty-popup', () => { + const target = document.getElementById('question'); + sox.helpers.observe(target, '#start-bounty-popup', () => { $('#start-bounty-popup').draggable({ drag: function() { $(this).css({ 'width': 'auto', 'height': 'auto', - }); }, }).css('cursor', 'move'); @@ -34,40 +34,41 @@ } }, - markEmployees: function() { + markEmployees: function () { // Description: Adds an Stack Overflow logo next to users that *ARE* a Stack Overflow Employee - const $links = $('.comment a, .deleted-answer-info a, .employee-name a, .user-details a').filter('a[href^="/users/"]'); + const anchors = [...document.querySelectorAll('.comment a, .deleted-answer-info a, .employee-name a, .user-details a, .question-summary .started a')].filter(el => { + return el.href.startsWith('/users/'); + }); const ids = []; - $links.each(function() { - const href = $(this).attr('href'); + for (let i = 0; i < anchors.length; i++) { + const href = anchors[i].href; const id = href.split('/')[2]; - if (ids.indexOf(id) === -1) ids.push(id); - }); - + } sox.debug('markEmployees user IDs', ids); - const url = 'https://api.stackexchange.com/2.2/users/{ids}?pagesize=100&site={site}&key={key}&access_token={access_token}' - .replace('{ids}', ids.join(';')) - .replace('{site}', sox.site.currentApiParameter) - .replace('{key}', sox.info.apikey) - .replace('{access_token}', sox.settings.accessToken); - - $.ajax({ - url: url, - success: function(data) { - sox.debug('markEmployees returned data', data); - for (let i = 0; i < data.items.length; i++) { - const userId = data.items[i].user_id; - const isEmployee = data.items[i].is_employee; - if (!isEmployee) return; - - $links.filter('a[href^="/users/' + userId + '/"]') - .after(''); - } - }, + // TODO is pagination needed? + sox.helpers.getFromAPI({ + endpoint: 'users', + ids, + sitename: sox.site.url, + filter: '!*MxJcsv91Tcz6yRH', + limit: 100, + featureId: 'markEmployees', + cacheDuration: 60 * 24, // Cache for 24 hours (in minutes) + }, items => { + sox.debug('markEmployees returned data', items); + for (let i = 0; i < items.length; i++) { + const userId = items[i].user_id; + const isEmployee = items[i].is_employee; + if (!isEmployee) continue; + + anchors.filter(el => el.href.startsWith(`/users/${userId}/`)).forEach(el => { + el.insertAdjacentHTML('afterend', ''); + }); + } }); }, @@ -75,51 +76,88 @@ // Description: Adds the 'show x more comments' link before the commnents // Test on e.g. https://meta.stackexchange.com/questions/125439/ - $('.js-show-link.comments-link').each(function() { - if (!$(this).parent().prev().find('.comment-text').length) return; //https://github.com/soscripted/sox/issues/196 + function copyLinks() { + $('.js-show-link.comments-link').each(function() { + if (!$(this).parent().prev().find('.comment-text').length) return; // https://github.com/soscripted/sox/issues/196 + + const $existingClonedBtn = $(this).parent().parent().find('.sox-copyCommentsLinkClone'); + if ($existingClonedBtn.length) { + if ($(this).parent().parent().find('.js-show-link.comments-link:visible').length !== 2) { + // If we've already cloned the button, we would expect 2 of the buttons to now exist + // If now, delete the clone as the comments have already been expanded (direct link to deep comment) + // See https://github.com/soscripted/sox/issues/379#issuecomment-460069876 + $existingClonedBtn.remove(); + } + // Don't add again if we've already cloned the button + return; + } - const $btnToAdd = $(this).clone(); - $btnToAdd.on('click', function(e) { - e.preventDefault(); - $(this).hide(); - }); + const $btnToAdd = $(this).clone(); + $btnToAdd.addClass('sox-copyCommentsLinkClone'); + $btnToAdd.on('click', function(e) { + e.preventDefault(); + $(this).hide(); + }); - $(this).parent().parent().prepend($btnToAdd); + $(this).parent().parent().prepend($btnToAdd); - $(this).click(() => { - $btnToAdd.hide(); + $(this).click(() => { + $btnToAdd.hide(); + }); }); - }); - $(document).on('click', '.js-add-link', function() { //https://github.com/soscripted/sox/issues/239 - const commentParent = ($(this).parents('.answer').length ? '.answer' : '.question'); - $(this).closest(commentParent).find('.js-show-link.comments-link').hide(); - }); + $(document).on('click', '.js-add-link', function() { //https://github.com/soscripted/sox/issues/239 + const commentParent = ($(this).parents('.answer').length ? '.answer' : '.question'); + $(this).closest(commentParent).find('.js-show-link.comments-link').hide(); + }); + } + + // copyLinks(); + $(document).on('sox-new-comment', copyLinks); }, highlightQuestions: function() { // Description: For highlighting only the tags of favorite questions function highlight() { - $('.tagged-interesting').removeClass('tagged-interesting sox-tagged-interesting').addClass('sox-tagged-interesting'); + const interestingQuestions = [...document.getElementsByClassName('tagged-interesting')]; + const questionsLength = interestingQuestions.length; + for (let i = 0; i < questionsLength; i++) { + const question = interestingQuestions[i]; + question.classList.remove('tagged-interesting'); + question.classList.add('sox-tagged-interesting'); + } } let color; - if (sox.location.on('superuser.com')) { //superuser + if (sox.location.on('superuser.com')) { color = '#00a1c9'; - } else if (sox.location.on('stackoverflow.com')) { //stackoverflow + } else if (sox.location.on('stackoverflow.com')) { color = '#f69c55'; } else if (sox.location.on('serverfault.com')) { color = '#EA292C'; - } else { //for all other sites - color = $('.post-tag').css('color'); + } else { + const existingPostTags = document.getElementsByClassName('post-tag'); + if (existingPostTags.length) { + color = existingPostTags[0].style.color; + } else { + // Default colour if we can't find one on the page already + color = '#39739d'; + } } - $('').appendTo('head'); + const style = document.createElement('style'); + style.type = 'text/css'; + style.appendChild(document.createTextNode(`.sox-tagged-interesting:before{ background: ${color} }`)); + document.head.appendChild(style); highlight(); - if ($('.question-summary').length) sox.helpers.observe('.question-summary', highlight); + if (document.getElementsByClassName('question-summary').length) { + const targetMainPage = document.getElementById('question-mini-list'); + const targetQuestionsPage = document.getElementById('questions'); + sox.helpers.observe([targetMainPage, targetQuestionsPage], '.question-summary', highlight); + } }, displayName: function() { @@ -139,13 +177,17 @@ // Description: For highlighting the names of answerers on comments function color() { - let answererID; - - $('.answercell').each(function() { - answererID = +this.querySelector('.post-signature:nth-last-of-type(1) a[href^="/users"]').href.match(/\d+/)[0]; - - $(this.nextElementSibling.querySelectorAll('.comment-user[href^="/users/' + answererID + '/"]')).addClass('sox-answerer'); - }); + const answerCells = [...document.getElementsByClassName('answercell')]; + for (let i = 0; i < answerCells.length; i++) { + const cell = answerCells[i]; + const answererID = cell.querySelector('.post-signature:nth-last-of-type(1) a').href.match(/\d+/)[0]; + const commentUsers = [...cell.nextElementSibling.getElementsByClassName('comment-user')] + .filter(c => c.href && c.href.contains(`/users/${answererID}/`)); + + for (let c = 0; c < commentUsers.length; c++) { + commentUsers[c].classList.add('sox-answerer'); + } + } } color(); @@ -247,25 +289,27 @@ const listBtn = '
  • '; function loopAndAddHandlers() { - $('[id^="wmd-redo-button"]').each(function() { - if (!this.dataset.kbdAdded) { - //compatability with https://stackapps.com/q/3341 as requested in https://github.com/soscripted/sox/issues/361 - //the SOX kbd button isn't added if that script is installed - if ($(this).parent().find('.tmAdded.wmd-kbd-button').length) { - $(this).after(listBtn); + const redoButtons = [...document.querySelectorAll('[id^="wmd-redo-button"]')]; + for (let i = 0; i < redoButtons.length; i++) { + const button = redoButtons[i]; + if (!button.dataset.kbdAdded) { + // Compatability with https://stackapps.com/q/3341 as requested in https://github.com/soscripted/sox/issues/361 + // The SOX kbd button isn't added if that script is installed + if (button.parentNode.querySelector('.tmAdded.wmd-kbd-button')) { + button.insertAdjacentHTML('afterend', listBtn); } else { - $(this).after(kbdBtn + listBtn); + button.insertAdjacentHTML('afterend', kbdBtn + listBtn); } - this.dataset.kbdAdded = true; + button.dataset.kbdAdded = true; } - }); + } } $(document).on('sox-edit-window', loopAndAddHandlers); loopAndAddHandlers(); - document.addEventListener('keydown', (event) => { + document.addEventListener('keydown', event => { const kC = event.keyCode; const target = event.target; @@ -304,7 +348,7 @@ // Old storage format was an array of arrays: [[name, text], [name, text], ...] // This converts it to an object (like DEFAULT_OPTIONS above) const newOptions = []; - options.forEach((opt) => { + options.forEach(opt => { newOptions.push({ [opt[0]]: opt[1], }); @@ -337,7 +381,6 @@ $('#currentValues').html(' '); const options = getOptions(); options.forEach(opt => { - console.log(opt); const [[name, text]] = Object.entries(opt); $('#currentValues').append(`
    @@ -379,25 +422,23 @@ } function addCheckboxes() { - sox.debug('editComment addCheckboxes() called'); const $editCommentField = $('input[id^="edit-comment"]'); //NOTE: input specifcally needed, due to https://github.com/soscripted/sox/issues/363 - sox.debug('editComment addCheckboxes() $editCommentField:', $editCommentField.get()); if (!$editCommentField.length) return; //https://github.com/soscripted/sox/issues/246 $('#reasons').remove(); //remove the div containing everything, we're going to add/remove stuff now: if (/\/edit/.test(sox.site.href) || $('[class^="inline-editor"]').length || $('.edit-comment').length) { - sox.debug('editComment addCheckboxes() adding boxes'); $editCommentField.after('
    '); + const $reasons = $('#reasons'); const options = getOptions(); options.forEach(opt => { const [[name, text]] = Object.entries(opt); - $('#reasons').append(` + $reasons.append(`   `); }); - $('#reasons input[type="checkbox"]').change(function() { + $reasons.find('input[type="checkbox"]').change(function() { if (this.checked) { //Add it to the summary if ($editCommentField.val()) { $editCommentField.val($editCommentField.val() + '; ' + $(this).val()); @@ -475,32 +516,44 @@ shareLinksPrivacy: function() { // Description: Remove your user ID from the 'share' link - sox.helpers.observe('.share-tip', () => { + const questionTarget = document.getElementById('question'); + const answersTargets = document.getElementsByClassName('answer'); + sox.helpers.observe([questionTarget, ...answersTargets], '.share-tip', () => { const toRemove = ' (includes your user id)'; - const popup = $('.share-tip'); - const origHtml = popup.html(); - if (origHtml.indexOf(toRemove) == -1) return; //don't do anything if the function's already done its thing + const popup = document.getElementsByClassName('share-tip')[0]; + if (!popup) return; - popup.html(() => origHtml.replace(toRemove, '')); + // Do nothing if the function's already done its thing + const origHtml = popup.innerHTML; + if (origHtml.indexOf(toRemove) == -1) return; - const inputBox = $('.share-tip input'); - const origLink = inputBox.val(); - inputBox.val(origLink.match(/.+\/(q|a)\/[0-9]+/g)); - inputBox.select(); + popup.innerHTML = origHtml.replace(toRemove, ''); + + const input = popup.querySelector('input'); + const origLink = input.value; + input.value = origLink.match(/.+\/(q|a)\/[0-9]+/g); + input.select(); }); }, shareLinksMarkdown: function() { // Description: For changing the 'share' button link to the format [name](link) - sox.helpers.observe('.share-tip', () => { - const link = $('.share-tip input').val(); - const title = $('meta[name="twitter:title"]').attr('content').replace(/\[(.*?)\]/g, '[$1]'); //https://github.com/soscripted/sox/issues/226, https://github.com/soscripted/sox/issues/292 + const questionTarget = document.getElementById('question'); + const answersTargets = document.getElementsByClassName('answer'); + sox.helpers.observe([questionTarget, ...answersTargets], '.share-tip', () => { + const input = document.querySelector('.share-tip input'); + if (!input) return; + + const title = document.querySelector('meta[name="twitter:title"]').getAttribute('content').replace(/\[(.*?)\]/g, '[$1]'); // https://github.com/soscripted/sox/issues/226, https://github.com/soscripted/sox/issues/292 + const link = input.value; + + // Do nothing if the function's already done its thing + if (link.indexOf(title) !== -1) return; - if (link.indexOf(title) !== -1) return; //don't do anything if the function's already done its thing - $('.share-tip input').val('[' + title + '](' + link + ')'); - $('.share-tip input').select(); - document.execCommand('copy'); //https://github.com/soscripted/sox/issues/177 + input.value = `[${title}](${link})`; + input.select(); + document.execCommand('copy'); // https://github.com/soscripted/sox/issues/177 }); }, @@ -547,8 +600,10 @@ // Description: For adding some text to spoilers to tell people to hover over it function addSpoilerTip() { - $('.spoiler').prepend('
    hover to show spoiler
    '); - $('.spoiler').hover(function() { + const $spoiler = $('.spoiler'); + + $spoiler.prepend('
    hover to show spoiler
    '); + $spoiler.hover(function() { $(this).find('#isSpoiler').hide(500); }, function() { $(this).find('#isSpoiler').show(500); @@ -564,17 +619,14 @@ if (!sox.user.loggedIn) return; function addReplyLinks() { - // Delay needed because of https://github.com/soscripted/sox/issues/379#issuecomment-460001854 - setTimeout(() => { - $('.comment').each(function () { - if (!$(this).find('.soxReplyLink').length) { //if the link doesn't already exist - if (sox.user.name !== $(this).find('.comment-text a.comment-user').text()) { //make sure the link is not added to your own comments - $(this).find('.comment-text').css('overflow-x', 'hidden'); - $(this).find('.comment-text .comment-body').append(''); - } + $('.comment').each(function () { + if (!$(this).find('.soxReplyLink').length) { //if the link doesn't already exist + if (sox.user.name !== $(this).find('.comment-text a.comment-user').text()) { //make sure the link is not added to your own comments + $(this).find('.comment-text').css('overflow-x', 'hidden'); + $(this).find('.comment-text .comment-body').append(''); } - }); - }, 100); + } + }); } $(document).on('click', 'span.soxReplyLink', function() { @@ -607,13 +659,20 @@ const href = this.href.replace(/https?:\/\//, '').replace(/www\./, ''); if (!href) return; - const siteName = sox.helpers.getSiteNameFromLink(href); + const sitename = sox.helpers.getSiteNameFromLink(href); const questionID = sox.helpers.getIDFromLink(href); // if it is a bare link is to a question on a SE site - if (questionID && siteName && isQuestionLink.test(href) && this.innerText.replace(/https?:\/\//, '').replace(/www\./, '') === href) { - sox.helpers.getFromAPI('questions', questionID, siteName, FILTER_QUESTION_TITLE, (json) => { - this.innerHTML = json.items[0].title; + if (questionID && sitename && isQuestionLink.test(href) && this.innerText.replace(/https?:\/\//, '').replace(/www\./, '') === href) { + sox.helpers.getFromAPI({ + endpoint: 'questions', + ids: questionID, + sitename, + filter: FILTER_QUESTION_TITLE, + featureId: 'parseCrossSiteLinks', + cacheDuration: 10, // Cache for 10 minutes + }, items => { + this.innerHTML = items[0].title; }); } }); @@ -628,7 +687,8 @@ if (window.location.href.indexOf('questions/') >= 0) { $(window).bind('beforeunload', () => { - if ($('.comment-form textarea').length && $('.comment-form textarea').val()) { + const textarea = document.querySelector('.comment-form textarea'); + if (textarea && textarea.value) { return 'Do you really want to navigate away? Anything you have written will be lost!'; } return; @@ -639,33 +699,36 @@ sortByBountyAmount: function() { // Description: For adding some buttons to sort bounty's by size - if ($('.bounty-indicator').length) { //if there is at least one bounty on the page - $('.question-summary').each(function() { - const bountyAmount = $(this).find('.bounty-indicator').text().replace('+', ''); - if (bountyAmount) { - $(this).attr('data-bountyamount', bountyAmount).addClass('hasBounty'); //add a 'bountyamount' attribute to all the questions - } - }); + // Do nothing unless there is at least one bounty on the page + if (!document.getElementsByClassName('bounty-indicator').length) return; - var $wrapper = $('#question-mini-list').length ? $('#question-mini-list') : $wrapper = $('#questions'); //homepage/questions tab + [...document.getElementsByClassName('question-summary')].forEach(summary => { + const indicator = summary.querySelector('.bounty-indicator'); + const bountyAmount = indicator ? indicator.innerText.replace('+', '') : undefined; + if (bountyAmount) { + summary.setAttribute('data-bountyamount', bountyAmount); + summary.classList.add('hasBounty'); // Add a 'bountyamount' attribute to all the questions + } + }); - //filter buttons: - $('.subheader').after('sort by bounty amount:   largest first  smallest first'); + // Homepage/questions tab + const wrapper = document.getElementById('question-mini-list') || document.getElementById('questions'); - //Thanks: http://stackoverflow.com/a/14160529/3541881 - $('#largestFirst').css('cursor', 'pointer').on('click', () => { //largest first - $wrapper.find('.question-summary.hasBounty').sort((a, b) => { - return +b.getAttribute('data-bountyamount') - +a.getAttribute('data-bountyamount'); - }).prependTo($wrapper); - }); + // Filter buttons: + $('.subheader').after('sort by bounty amount:   largest first  smallest first'); - //Thanks: http://stackoverflow.com/a/14160529/3541881 - $('#smallestFirst').css('cursor', 'pointer').on('click', () => { //smallest first - $wrapper.find('.question-summary.hasBounty').sort((a, b) => { - return +a.getAttribute('data-bountyamount') - +b.getAttribute('data-bountyamount'); - }).prependTo($wrapper); - }); - } + // Thanks: http://stackoverflow.com/a/14160529/3541881 + $('#largestFirst').css('cursor', 'pointer').on('click', () => { // Largest first + [...wrapper.querySelectorAll('.question-summary.hasBounty')].sort((a, b) => { + return +b.getAttribute('data-bountyamount') - +a.getAttribute('data-bountyamount'); + }).prependTo($(wrapper)); + }); + + $('#smallestFirst').css('cursor', 'pointer').on('click', () => { // Smallest first + [...wrapper.querySelectorAll('.question-summary.hasBounty')].sort((a, b) => { + return +a.getAttribute('data-bountyamount') - +b.getAttribute('data-bountyamount'); + }).prependTo($(wrapper)); + }); }, isQuestionHot: function() { @@ -686,7 +749,7 @@ const hnqJSONUrl = 'https://stackexchange.com/hot-questions-for-mobile'; const requestUrl = proxyUrl + hnqJSONUrl; - $.get(requestUrl, (results) => { + $.get(requestUrl, results => { if (sox.location.on('/questions/')) { $.each(results, (i, o) => { if (document.URL.indexOf(o.site + '/questions/' + o.question_id) > -1) addHotText(); @@ -694,7 +757,7 @@ } else { $('.question-summary').each(function() { const id = $(this).attr('id').split('-')[2]; - if (results.filter((d) => { + if (results.filter(d => { return d.question_id == id; }).length) { $(this).find('.summary h3').prepend('
    '); @@ -794,14 +857,21 @@ }); $('.showCommentScore').css('cursor', 'pointer').on('click', function() { - sox.helpers.getFromAPI('comments', this.id, sitename, COMMENT_SCORE_FILTER, (json) => { - this.innerHTML = WHITESPACES + json.items[0].score; + sox.helpers.getFromAPI({ + endpoint: 'comments', + ids: this.id, + sitename, + filter: COMMENT_SCORE_FILTER, + useCache: false, // Single ID, so no point + }, items => { + this.innerHTML = WHITESPACES + items[0].score; }); }); } addLabelsAndHandlers(); - sox.helpers.observe('.history-table', addLabelsAndHandlers); + const target = document.getElementById('mainbar-full'); + if (target) sox.helpers.observe(target, '.history-table', addLabelsAndHandlers); }, answerTagsSearch: function() { @@ -819,50 +889,48 @@ } } - const sitename = sox.site.currentApiParameter; - const questionIDs = []; - - // questionID: [tagArray, insertedTagDOM] - // second element is for caching, in case more than - // one answer in the search list belongs to the same question - let questionID; - const tagsForQuestionIDs = {}; const QUESTION_TAGS_FILTER = '!)8aDT8Opwq-vdo8'; + const questionIDs = []; + + const answers = [...document.getElementsByClassName('question-summary')].filter(q => /answer-id/.test(q.id)); - // get corresponding question's ID for each answer - $('div[id*="answer"]').each(function() { - questionID = getQuestionIDFromAnswerDIV(this); - // cache value for later reference - this.dataset.questionid = questionID; + // Get corresponding question's ID for each answer + answers.forEach(answer => { + const questionID = getQuestionIDFromAnswerDIV(answer); + // Cache value for later reference + answer.dataset.questionid = questionID; questionIDs.push(questionID); }); - sox.helpers.getFromAPI('questions', questionIDs.join(';'), sitename, QUESTION_TAGS_FILTER, (json) => { - const items = json.items; + sox.helpers.getFromAPI({ + endpoint: 'questions', + ids: questionIDs, + sitename: sox.site.currentApiParameter, + filter: QUESTION_TAGS_FILTER, + limit: 60, + sort: 'creation', + featureId: 'answerTagsSearch', + cacheDuration: 10, // Cache for 10 minutes + }, items => { const itemsLength = items.length; - let item; for (let i = 0; i < itemsLength; i++) { - item = items[i]; - tagsForQuestionIDs[item.question_id] = [item.tags, null]; + const item = items[i]; + tagsForQuestionIDs[item.question_id] = item.tags; } - $('div[id*="answer"]').each(function() { - const $this = $(this); - const id = +this.dataset.questionid; - const tagsForThisQuestion = tagsForQuestionIDs[id][0]; - - let currTag; - let $insertedTag = tagsForQuestionIDs[id][1]; + answers.forEach(answer => { + const id = +answer.dataset.questionid; + const tagsForThisQuestion = tagsForQuestionIDs[id]; for (let x = 0; x < tagsForThisQuestion.length; x++) { - currTag = tagsForThisQuestion[x]; - $insertedTag = $this.find('.summary .tags').append(''); + const currTag = tagsForThisQuestion[x]; + const $insertedTag = $(answer.querySelector('.summary .tags')).append(''); addClassToInsertedTag($insertedTag); } }); - }, 'creation&pagesize=60'); + }); }, stickyVoteButtons: function() { @@ -872,6 +940,7 @@ $('.votecell > .js-voting-container').css({ //.votecell is necessary; e.g., the number of votes of questions on the Questions list for a site uses the .vote class too 'position': '-webkit-sticky', + // eslint-disable-next-line no-dupe-keys 'position': 'sticky', 'top': parseInt($('.container').css('margin-top'), 10) + parseInt($('body').css('padding-top'), 10), //Seems like most sites use margin-top on the container, but Meta and SO use padding on the body }); @@ -881,33 +950,41 @@ // Description: For showing the new version of a title in a diff separately rather than loads of crossing outs in red and additions in green function betterTitle() { - sox.debug('ran betterTitle from titleEditDiff'); - const $questionHyperlink = $('.summary h2 .question-hyperlink').clone(); - const $questionHyperlinkTwo = $('.summary h2 .question-hyperlink').clone(); - const link = $('.summary h2 .question-hyperlink').attr('href'); + sox.debug('titleEditDiff: running betterTitle'); + const $questionHyperlinkOriginal = $('.summary h2 .question-hyperlink'); + const $questionHyperlink = $questionHyperlinkOriginal.clone(); + const $questionHyperlinkTwo = $questionHyperlinkOriginal.clone(); + + const link = $questionHyperlinkOriginal.attr('href'); const added = ($questionHyperlinkTwo.find('.diff-delete').remove().end().text()); const removed = ($questionHyperlink.find('.diff-add').remove().end().text()); - if ($('.summary h2 .question-hyperlink').find('.diff-delete, .diff-add').length && !($('.sox-better-title').length)) { - if (!$('.sox-better-title-toggle').length) $('.summary h2 .question-hyperlink').before(''); - $('.summary h2 .question-hyperlink').addClass('sox-original-title-diff').hide(); - $('.summary h2 .question-hyperlink').after('' + removed + '' + added + ''); + if ($questionHyperlinkOriginal.find('.diff-delete, .diff-add').length && !($('.sox-better-title').length)) { + if (!$('.sox-better-title-toggle').length) { + $('.summary h2 .question-hyperlink').before(''); + } + $questionHyperlinkOriginal.addClass('sox-original-title-diff').hide(); + $questionHyperlinkOriginal.after('' + removed + '' + added + ''); } } - betterTitle(); - sox.helpers.observe('.review-status, .review-content, .suggested-edit, .post-id', betterTitle); - $(document).on('click', '.sox-better-title-toggle', function() { //https://github.com/soscripted/sox/issues/166#issuecomment-269925059 - if ($('.sox-original-title-diff').is(':visible')) { + // https://github.com/soscripted/sox/issues/166#issuecomment-269925059 + $(document).on('click', '.sox-better-title-toggle', function() { + const $soxOriginalTitleDiff = $('.sox-original-title-diff'); + if ($soxOriginalTitleDiff.is(':visible')) { $(this).addClass('fa-toggle-on').removeClass('fa-toggle-off'); - $('.sox-original-title-diff').hide(); + $soxOriginalTitleDiff.hide(); $('.sox-better-title').show(); } else { $(this).removeClass('fa-toggle-on').addClass('fa-toggle-off'); - $('.sox-original-title-diff').show(); + $soxOriginalTitleDiff.show(); $('.sox-better-title').hide(); } }); + + betterTitle(); + const target = document.querySelector('.review-content'); + sox.helpers.observe(target, '.review-status, .review-content, .suggested-edit, .post-id', betterTitle); }, metaChatBlogStackExchangeButton: function() { @@ -958,30 +1035,29 @@ //Do not run on meta, chat, or sites without a meta if ((sox.site.type != 'main' && sox.site.type != 'beta') || !$('.related-site').length) return; - var NEWQUESTIONS = 'metaNewQuestionAlert-lastQuestions'; - var favicon = sox.site.icon; - var metaName = 'meta.' + sox.site.currentApiParameter; - var lastQuestions = {}; - var FILTER_QUESTION_TITLE_LINK = '!BHMIbze0EQ*ved8LyoO6rNk25qGESy'; - var $dialog = $('
    ', { + const NEWQUESTIONS = 'metaNewQuestionAlert-lastQuestions'; + const favicon = sox.site.icon; + const metaName = 'meta.' + sox.site.currentApiParameter; + const FILTER_QUESTION_TITLE_LINK = '!BHMIbze0EQ*ved8LyoO6rNk25qGESy'; + const $dialog = $('
    ', { id: 'metaNewQuestionAlertDialog', 'class': 'topbar-dialog dno new-topbar', }); - var $header = $('
    ', { + const $header = $('
    ', { 'class': 'header', }).append($('

    ').append($('', { text: 'new meta posts', href: `//meta.${sox.site.url}`, style: 'color: #0077cc', }))); - var $content = $('
    ', { + const $content = $('
    ', { 'class': 'modal-content', }); - var $questions = $('
      ', { + const $questions = $('
        ', { id: 'metaNewQuestionAlertDialogList', 'class': 'js-items items', }); - var $diamond = $('', { + const $diamond = $('', { id: 'metaNewQuestionAlertButton', href: '#', 'class': '-link', @@ -1000,6 +1076,7 @@ }).append($('', { d: 'M8.4.78c.33-.43.87-.43 1.3 0l5.8 7.44c.33.43.33 1.13 0 1.56l-5.8 7.44c-.33.43-.87.43-1.2 0L2.6 9.78a1.34 1.34 0 0 1 0-0.156L8.4.78z', }))); + let lastQuestions = {}; $diamond.html($diamond.html()); //Reloads the diamond icon, which is necessary when adding an SVG using jQuery. @@ -1008,12 +1085,12 @@ $dialog.css({ 'top': $('.top-bar').height(), - 'right': $('.-container').outerWidth() - $('#metaNewQuestionAlertButton').parent().position().left - $('#metaNewQuestionAlertButton').outerWidth(), + 'right': $('.-container').outerWidth() - $diamond.parent().position().left - $diamond.outerWidth(), }); if ($('#metaNewQuestionAlertButton').length) $('.js-topbar-dialog-corral').append($dialog); - $(document).mouseup((e) => { + $(document).mouseup(e => { if (!$dialog.is(e.target) && $dialog.has(e.target).length === 0 && !$(e.target).is('#metaNewQuestionAlertButton, svg, path')) { @@ -1028,8 +1105,14 @@ lastQuestions = JSON.parse(GM_getValue(NEWQUESTIONS)); } - sox.helpers.getFromAPI('questions', false, metaName, FILTER_QUESTION_TITLE_LINK, (json) => { - const items = json.items; + sox.helpers.getFromAPI({ + endpoint: 'questions', + sitename: metaName, + filter: FILTER_QUESTION_TITLE_LINK, + sort: 'activity', + limit: 5, + featureId: 'metaNewQuestionAlert', + }, items => { const latestQuestion = items[0].title; // Make diamond blue if there's a new question @@ -1052,7 +1135,7 @@ $diamond.click(() => { GM_setValue(NEWQUESTIONS, JSON.stringify(lastQuestions)); }); - }, 'activity&pagesize=5'); + }); function addQuestion(title, link, seen) { const $li = $('
      • '); @@ -1081,7 +1164,7 @@ function addCSS() { $('.js-vote-up-btn, .js-vote-down-btn, .js-favorite-btn').addClass('sox-better-css'); - $('head').append(''); + $('head').append(''); $('#hmenus').css('-webkit-transform', 'translateZ(0)'); } addCSS(); @@ -1091,110 +1174,143 @@ standOutDupeCloseMigrated: function() { // Description: For adding cooler signs that a questions has been closed/migrated/put on hod/is a dupe - // for use in dataset - // used for hideCertainQuestions feature compatability + // For use in dataset, used for hideCertainQuestions feature compatability const QUESTION_STATE_KEY = 'soxQuestionState'; const FILTER_QUESTION_CLOSURE_NOTICE = '!)Ei)3K*irDvFA)l92Lld3zD9Mu9KMQ59-bgpVw7D9ngv5zEt3'; + const NOTICE_REGEX = /\[(duplicate|closed|migrated|on hold)\]$/; - function addLabel(index, question) { - // don't run if question already has tag added - if (question.dataset[QUESTION_STATE_KEY]) return; - - const $anchor = $(question.querySelector('.summary h3 a')); - const text = $anchor.text().trim(); - const id = sox.helpers.getIDFromAnchor($anchor[0]); - - //https://github.com/soscripted/sox/issues/181 - $('.question-summary .answer-hyperlink, .question-summary .question-hyperlink, .question-summary .result-link a').css('display', 'inline'); - $('.summary h3').css('line-height', '1.2em'); //fixes line height on "Questions" page + function addLabels() { + const questions = []; + const questionSummaries = [...document.getElementsByClassName('question-summary')]; + questionSummaries.forEach(question => { + // Don't run if tag has already been added to question + if (question.dataset[QUESTION_STATE_KEY]) return; - const noticeRegex = /\[(duplicate|closed|migrated|on hold)\]$/; - const noticeMatch = text.match(noticeRegex); - const noticeName = noticeMatch && noticeMatch[1]; - const queryType = 'questions'; - - if (!noticeName) return; - - $anchor.text(text.replace(noticeRegex, '')); - question.dataset[QUESTION_STATE_KEY] = noticeName; - - switch (noticeName) { - case 'duplicate': - sox.helpers.getFromAPI(queryType, id, sox.site.currentApiParameter, FILTER_QUESTION_CLOSURE_NOTICE, (data) => { - const question = data.items[0]; - const questionId = question.closed_details.original_questions[0].question_id; + const anchor = question.querySelector('.summary h3 a'); + const id = sox.helpers.getIDFromAnchor(anchor); + const text = anchor.innerText.trim(); - //styling for https://github.com/soscripted/sox/issues/181 + const noticeMatch = text.match(NOTICE_REGEX); + const noticeName = noticeMatch && noticeMatch[1]; - //NOTE: the `data-searchsession` attribute is to workaround a weird line of code in SE *search* pages, - //which changes the `href` of anchors in in `.result-link` containers to `data-searchsession` - //See https://github.com/soscripted/sox/pull/348#issuecomment-404245056 - $anchor.after('  duplicate '); - }); - break; - case 'closed': - case 'on hold': - sox.helpers.getFromAPI(queryType, id, sox.site.currentApiParameter, FILTER_QUESTION_CLOSURE_NOTICE, (data) => { - const question = data.items[0]; - - const details = question.closed_details; - const users = details.by_users.reduce((str, user) => str + ', ' + user.display_name, '').substr(2); - const closureDate = new Date(question.closed_date * 1000); - const timestamp = closureDate.toLocaleString(); - const closeNotice = (details.on_hold ? 'put on hold' : 'closed') + ' as '; - const closeText = details.on_hold ? 'on hold' : 'closed'; - const cssClass = details.on_hold ? 'onhold' : 'closed'; - - $anchor.after('  ' + closeText + ' '); - }); - break; - case 'migrated': - sox.helpers.getFromAPI('questions', id, sox.site.currentApiParameter, FILTER_QUESTION_CLOSURE_NOTICE, (data) => { - const question = data.items[0]; + // Don't run if the question is still open + if (!noticeName) return; + questions.push({ element: question, noticeName, text, anchor, id }); + }); + sox.debug('standOutDupeCloseMigrated questions to request API for', questions); - const migratedToSite = question.migrated_to.other_site.name; - const textToAdd = 'migrated to ' + migratedToSite; + // https://github.com/soscripted/sox/issues/181 + $('.question-summary .answer-hyperlink, .question-summary .question-hyperlink, .question-summary .result-link a').css('display', 'inline'); + $('.summary h3').css('line-height', '1.2em'); // Fixes line height on "Questions" page + + sox.helpers.getFromAPI({ + endpoint: 'questions', + ids: questions.map(q => q.id), + sitename: sox.site.currentApiParameter, + filter: FILTER_QUESTION_CLOSURE_NOTICE, + featureId: 'standOutDupeCloseMigrated', + cacheDuration: 10, // Cache for 10 minutes + }, items => { + questions.forEach(question => { + sox.debug('standOutDupeCloseMigrated adding details for question', question); + question.anchor.innerText = question.text.replace(NOTICE_REGEX, ''); + question.element.dataset[QUESTION_STATE_KEY] = question.noticeName; + + const questionDetails = items.find(d => d.question_id === question.id); + if (!questionDetails) return; + + switch (question.noticeName) { + case 'duplicate': { + const questionId = questionDetails.closed_details.original_questions[0].question_id; + + // Styling for https://github.com/soscripted/sox/issues/181 + // NOTE: the `data-searchsession` attribute is to workaround a weird line of code in SE *search* pages, + // which changes the `href` of anchors in in `.result-link` containers to `data-searchsession` + // See https://github.com/soscripted/sox/pull/348#issuecomment-404245056 + $(question.anchor).after('  duplicate '); + break; + } + case 'closed': + case 'on hold': { + const details = questionDetails.closed_details; + const users = details.by_users.reduce((str, user) => str + ', ' + user.display_name, '').substr(2); + const closureDate = new Date(questionDetails.closed_date * 1000); + const timestamp = closureDate.toLocaleString(); + const closeNotice = (details.on_hold ? 'put on hold' : 'closed') + ' as '; + const closeText = details.on_hold ? 'on hold' : 'closed'; + const cssClass = details.on_hold ? 'onhold' : 'closed'; + + $(question.anchor).after('  ' + closeText + ' '); + break; + } + case 'migrated': { + let textToAdd; + if (questionDetails.migrated_to) { + const migratedToSite = questionDetails.migrated_to.other_site.name; + textToAdd = 'migrated to ' + migratedToSite; + } else if (questionDetails.migrated_from) { + const migratedFromSite = questionDetails.migrated_from.other_site.name; + textToAdd = 'migrated from ' + migratedFromSite; + } else { + sox.warn('standOutDupeCloseMigrated: unknown migration state'); + } - $anchor.after('  migrated '); + $(question.anchor).after('  migrated '); + break; + } + } }); - break; - } + }); } - // Find the questions and add their id's and statuses to an object - $('.question-summary').each(addLabel); + addLabels(); - sox.helpers.observe('#user-tab-questions, #question-mini-list', () => { //new questions on homepage, or for on user profile page - $('.question-summary').each(addLabel); - }); + const targetMainPage = document.getElementById('question-mini-list'); + const targetQuestionsPage = document.getElementById('questions'); + sox.helpers.observe([targetMainPage, targetQuestionsPage], '#questions, #question-mini-list', addLabels); }, editReasonTooltip: function() { // Description: For showing the latest revision's comment as a tooltip on 'edit [date] at [time]' - function getComment(url, $that) { - $.get(url, (responseText, textStatus, XMLHttpRequest) => { - sox.debug('SOX editReasonTooltip URL: ' + url); - sox.debug('SOX editReasonTooltip text: ' + $(XMLHttpRequest.responseText).find('.revision-comment:eq(0)')[0].innerHTML); - sox.debug('SOX editReasonTooltip: adding to tooltip'); - $that.find('.sox-revision-comment').attr('title', $(XMLHttpRequest.responseText).find('.revision-comment:eq(0)')[0].innerHTML); - sox.debug('SOX editReasonTooltip: finished adding to tooltip'); - sox.debug('SOX editReasonTooltip: tooltip is now: ' + $that.find('.sox-revision-comment').attr('title')); - }); - } - - function loopAndAddTooltip() { + function addTooltips() { + const ids = []; + const $posts = []; $('.question, .answer').each(function() { if ($(this).find('.post-signature').length > 1) { + $posts.push($(this)); const id = $(this).attr('data-questionid') || $(this).attr('data-answerid'); - $(this).find('.post-signature:eq(0)').find('.user-action-time a').wrapInner(''); - const $that = $(this); - getComment(location.protocol + '//' + sox.site.url + '/posts/' + id + '/revisions', $that); + ids.push(id); } }); + if (!ids.length) return; + + sox.helpers.getFromAPI({ + endpoint: 'posts', + childEndpoint: 'revisions', + sitename: sox.site.url, + filter: '!SWJaL02RNFkXc_we4i', + ids, + featureId: 'editReasonTooltip', + cacheDuration: 5, // Cache for 5 minutes + }, revisions => { + $posts.forEach($post => { + const id = $post.attr('data-questionid') || $post.attr('data-answerid'); + const revision = revisions.find(r => r.revision_type === 'single_user' && r.post_id === +id); + if (revision) { + const span = sox.helpers.newElement('span', { + 'class': 'sox-revision-comment', + 'title': revision.comment, + }); + sox.debug(`editReasonTooltip, adding text to tooltip for post ${id}: '${revision.comment}'`); + $post.find('.post-signature:eq(0)').find('.user-action-time a').wrapInner(span); + } + }); + }); } - loopAndAddTooltip(); - $(document).on('sox-new-review-post-appeared', loopAndAddTooltip); + + addTooltips(); + $(document).on('sox-new-review-post-appeared', addTooltips); }, addSBSBtn: function(settings) { @@ -1319,14 +1435,16 @@ const numAnchors = anchorList.length; const itemIDs = []; - for (var i = 1; i <= numAnchors - 2; i++) { //the first and last anchors aren't answers + for (let i = 1; i <= numAnchors - 2; i++) { //the first and last anchors aren't answers itemIDs.push(anchorList[i].name); } itemIDs.push($('.question').data('questionid')); //event listeners for adding the sbs toggle buttons for editing existing questions or answers - for (i = 0; i <= numAnchors - 2; i++) { - sox.helpers.observe('#wmd-redo-button-' + itemIDs[i], SBS); + const targetQuestionCells = document.getElementsByClassName('postcell'); + const targetAnswerCells = document.getElementsByClassName('answercell'); + for (let i = 0; i <= numAnchors - 2; i++) { + sox.helpers.observe([...targetAnswerCells, ...targetQuestionCells], '#wmd-redo-button-' + itemIDs[i], SBS); } } @@ -1338,15 +1456,6 @@ }, - alwaysShowImageUploadLinkBox: function() { - // Description: For always showing the 'Link from the web' box when uploading an image. - - sox.helpers.observe('.image-upload', () => { - const toClick = $('.image-upload form div.modal-options-default.tab-page > a'); - if (toClick.length) toClick[0].click(); - }); - }, - addAuthorNameToInboxNotifications: function(settings) { // Description: To add the author's name to inbox notifications function setAuthorName(node) { @@ -1383,14 +1492,20 @@ sox.debug('addAuthorNameToInboxNotifications: ', node, id); - sox.helpers.getFromAPI(apiCallType, id, sitename, filter, (json) => { - sox.debug('addAuthorNameToInboxNotifications JSON returned from API', json); + sox.helpers.getFromAPI({ + endpoint: apiCallType, + ids: id, + sitename, + filter, + useCache: false, // Single ID so no point + }, items => { + sox.debug('addAuthorNameToInboxNotifications JSON returned from API', items); // https://github.com/soscripted/sox/issues/233 const temporaryDIV = $('
        '); - if (!json.items.length) return; + if (!items.length) return; - const author = (link.indexOf('/suggested-edits/') > -1 ? json.items[0].proposing_user.display_name : json.items[0].owner.display_name); + const author = (link.indexOf('/suggested-edits/') > -1 ? items[0].proposing_user.display_name : items[0].owner.display_name); const $author = $('', { class: 'sox-notification-author', text: (prependToMessage ? '' : ' by ') + temporaryDIV.html(author).text() + (prependToMessage ? ': ' : ''), //https://github.com/soscripted/sox/issues/347 @@ -1413,19 +1528,21 @@ const PROCESSED_CLASS = 'sox-authorNameAdded'; const MAX_PROCESSED_AT_ONCE = 20; - sox.helpers.observe('.' + inboxClass, () => { - const inboxDialog = document.getElementsByClassName(inboxClass)[0]; - const unprocessedElements = inboxDialog.querySelectorAll('.inbox-item:not(.' + PROCESSED_CLASS + ')'); - const lim = Math.min(unprocessedElements.length, MAX_PROCESSED_AT_ONCE); - - let element; - - for (let x = 0; x < lim; x++) { - element = unprocessedElements[x]; - setAuthorName(element); - element.classList.add(PROCESSED_CLASS); - } - }); + const target = document.querySelector('.-dialog-container'); + if (target) { + sox.helpers.observe(target, '.inbox-item', () => { + const inboxDialog = document.getElementsByClassName(inboxClass)[0]; + let eligibleElements = [...inboxDialog.querySelectorAll('.inbox-item')]; + eligibleElements = eligibleElements.slice(0, MAX_PROCESSED_AT_ONCE); + + const unprocessedElements = eligibleElements.filter(e => !e.classList.contains(PROCESSED_CLASS)); + for (let x = 0; x < unprocessedElements.length; x++) { + const element = unprocessedElements[x]; + setAuthorName(element); + element.classList.add(PROCESSED_CLASS); + } + }); + } }, flagOutcomeTime: function() { @@ -1476,7 +1593,6 @@ COMMENT: 4, }; const type = { - TOTAL: 'flags', WAITING: 'waiting', HELPFUL: 'helpful', DECLINED: 'declined', @@ -1490,10 +1606,10 @@ function addPercentage(group, type, percentage) { const $span = $('', { - text: '({0}%)'.replace('{0}', percentage), + text: `(${percentage}%)`, style: 'margin-left:5px; color: #999; font-size: 12px;', }); - $('td > a[href*="group=' + group + '"]:contains("' + type + '")').after($span); + $(`li > a[href*="group=${group}"]:contains("${type}")`).find('div:first').after($span); } function calculatePercentage(count, total) { @@ -1503,9 +1619,9 @@ function getFlagCount(group, type) { let flagCount = 0; - flagCount += Number($('td > a[href*="group=' + group + '"]:contains("' + type + '")') - .parent() - .prev() + const $groupHeader = $(`li > a[href="?group=${group}"]`); + flagCount += Number($groupHeader.next().find(`li:contains("${type}")`) + .find('div:last') .text() .replace(',', '')); return flagCount; @@ -1514,16 +1630,13 @@ // add percentages for (const groupKey in group) { const item = group[groupKey]; + const total = +$(`li > a[href="?group=${item}"]`).find('div:last').text(); - const total = getFlagCount(item, type.TOTAL); for (const typeKey in type) { const typeItem = type[typeKey]; - if (typeKey !== 'TOTAL') { - count = getFlagCount(item, typeItem); - percentage = calculatePercentage(count, total); - //sox.debug(groupKey + ": " + typeKey + " Flags -- " + count); - addPercentage(item, typeItem, percentage); - } + count = getFlagCount(item, typeItem); + percentage = calculatePercentage(count, total); + addPercentage(item, typeItem, percentage); } } }, @@ -1532,35 +1645,30 @@ // Description: Displays linked posts inline with an arrow function getIdFromUrl(url) { - if (url.indexOf('/questions/tagged/') !== -1) return false; - - if (url.indexOf('/a/') > -1) { //eg. http://meta.stackexchange.com/a/26764/260841 - return url.split('/a/')[1].split('/')[0]; - - } else if (url.indexOf('/q/') > -1) { //eg. http://meta.stackexchange.com/q/26756/260841 - return url.split('/q/')[1].split('/')[0]; - - } else if (url.indexOf('/questions/') > -1) { - if (url.indexOf('#') > -1) { //then it's probably an answer, eg. http://meta.stackexchange.com/questions/26756/how-do-i-use-a-small-font-size-in-questions-and-answers/26764#26764 - return url.split('#')[1]; - - } else { //then it's a question - return url.split('/questions/')[1].split('/')[0]; - } + let idMatch; + if (url.match('/a|q/')) { + // eg. http://meta.stackexchange.com/a/26764/260841 or http://meta.stackexchange.com/q/26756/260841 + idMatch = url.match(/\/(?:a|q)\/(\d+)/); + } else if (url.includes('/questions/')) { + // If URL includes '#', probably an answer, eg. http://meta.stackexchange.com/questions/26756/how-do-i-use-a-small-font-size-in-questions-and-answers/26764#26764 + // Oherwise, it's a question + idMatch = url.match(/#?(\d+)/); } + if (idMatch) return idMatch[1]; } function addButton() { $('.post-text a, .comments .comment-copy a').each(function() { const url = $(this).attr('href'); - //https://github.com/soscripted/sox/issues/205 -- check link's location is to same site, eg if on SU, don't allow on M.SU - //http://stackoverflow.com/a/4815665/3541881 + // https://github.com/soscripted/sox/issues/205 -- check link's location is to same site, eg if on SU, don't allow on M.SU + // http://stackoverflow.com/a/4815665/3541881 if (url && $('').prop('href', url).prop('hostname') == location.hostname && - url.indexOf('#comment') == -1 && - url.indexOf('/edit') == -1 && //https://github.com/soscripted/sox/issues/281 - getIdFromUrl(url) && //getIdFromUrl(url) makes sure it won't fail later on + !url.includes('#comment') && + !url.includes('/edit/') && // https://github.com/soscripted/sox/issues/281 + !url.includes('/tagged/') && + getIdFromUrl(url) && // getIdFromUrl(url) makes sure it won't fail later on !$(this).prev().is('.expand-post-sox')) { $(this).before(''); } @@ -1580,7 +1688,7 @@ $(this).addClass('expander-arrow-small-show'); const $that = $(this); const id = getIdFromUrl($(this).next().attr('href')); - $.get(location.protocol + '//' + sox.site.url + '/posts/' + id + '/body', (d) => { + $.get(location.protocol + '//' + sox.site.url + '/posts/' + id + '/body', d => { const div = '
        ' + d + '
        '; $that.next().after(div); }); @@ -1588,12 +1696,6 @@ }); }, - hideHotNetworkQuestions: function() { - // Description: Hides the Hot Network Questions module from the sidebar - - $('#hot-network-questions').remove(); - }, - hideHireMe: function() { // Description: Hides the Looking for a Job module from the sidebar @@ -1623,15 +1725,16 @@ }, hideLoveThisSite: function() { - // Description: Hides the "Love This Site?"" module from the sidebar + // Description: Hides the "Love This Site?" (weekly newsletter) module from the sidebar - $('#sidebar #newsletter-ad').parent().remove(); + $('#sidebar #newsletter-ad').remove(); }, chatEasyAccess: function() { // Description: Adds options to give a user read/write/no access in chat from their user popup dialog - sox.helpers.observe('.user-popup', (node) => { + const target = document.getElementById('chat-body'); + sox.helpers.observe(target, '.user-popup', node => { const $node = $(node).parent(); const id = $node.find('a')[0].href.split('/')[4]; @@ -1691,9 +1794,7 @@ $(':not(.deleted-answer) .answercell').slice(1).sort(compareByScore).slice(0, 5).each(function() { count++; const id = $(this).find('.short-link').attr('id').replace('link-post-', ''); - const score = $(this).prev().find('.js-vote-count').text(); - let icon = 'vote-up-off'; if (score > 0) { @@ -1720,29 +1821,25 @@ $column.append($link).appendTo($row); }); - if (count > 0) { + if (count > 0 && !$('#sox-top-answers').length) { $('#answers div.answer:first').before($topAnswers); $table.css('width', count * 100 + 'px'); } }, tabularReviewerStats: function() { - // Description: Adds a notification to the inbox if a question you downvoted and watched is edited + // Description: Display reviewer stats on /review/suggested-edits in table form // Idea by lolreppeatlol @ http://meta.stackexchange.com/a/277446/260841 :) - sox.helpers.observe('.review-more-instructions', () => { + const target = document.querySelector('.mainbar-full'); + sox.helpers.observe(target, '.review-more-instructions', () => { const info = {}; $('.review-more-instructions ul:eq(0) li').each(function() { const text = $(this).text(); - const username = $(this).find('a').text(); - const link = $(this).find('a').attr('href'); - const approved = text.match(/approved (.*?)[a-zA-Z]/)[1]; - const rejected = text.match(/rejected (.*?)[a-zA-Z]/)[1]; - const improved = text.match(/improved (.*?)[a-zA-Z]/)[1]; info[username] = { 'link': link, @@ -1752,14 +1849,11 @@ }; }); const $editor = $('.review-more-instructions ul:eq(1) li'); - + if (!$editor.length) return; const editorName = $editor.find('a').text(); - const editorLink = $editor.find('a').attr('href'); - const editorApproved = $editor.clone().find('a').remove().end().text().match(/([0-9]+)/g)[0]; //`+` matches 'one or more' to make sure it works on multi-digit numbers! - const editorRejected = $editor.clone().find('a').remove().end().text().match(/([0-9]+)/g)[1]; //https://stackoverflow.com/q/11347779/3541881 for fixing https://github.com/soscripted/sox/issues/279 info[editorName] = { 'link': editorLink, @@ -1779,26 +1873,38 @@ linkedToFrom: function() { // Description: Add an arrow to linked posts in the sidebar to show whether they are linked to or linked from - const currentId = location.href.split('/')[4]; - $('.linked .spacer a.question-hyperlink').each(function() { - const id = $(this).attr('href').split('/')[4]; - if ($('a[href*="' + id + '"]').not('.spacer a').length) { - const $that = $(this); - $that.append(''); - $.ajax({ - url: '/questions/' + id, - type: 'get', - dataType: 'html', - async: 'false', - success: function(d) { - if ($(d).find('a[href*="' + currentId + '"]').not('.spacer a').length) { - $that.append(''); - } - }, - }); - } else { - $(this).append(''); - } + const currentPageId = +location.href.match(/\/(\d+)\//)[1]; + sox.helpers.getFromAPI({ + endpoint: 'questions', + childEndpoint: 'linked', + ids: currentPageId, + sitename: sox.site.url, + filter: '!-MOiNm40Dv9qWI4dBqjO5FBS8p*ODCWqP', + featureId: 'linkedToFrom', + cacheDuration: 30, // Cache for 30 minutes + }, pagesThatLinkToThisPage => { + $('.linked .spacer a.question-hyperlink').each(function () { + const id = +$(this).attr('href').match(/\/(\d+)\//)[1]; + + if ($('a[href*="' + id + '"]').not('#sidebar a').length) { + // If a link from 'linked questions' does exist elsewhere on this page + // Then we know that this page definitely references the linked post + $(this).append(''); + + // However, the linked post _might_ also reference the current page, so let's check: + if (pagesThatLinkToThisPage.find(question => question.question_id === currentPageId && question.qustion_id === id)) { + // The current page is linked to from question_id (which is also the current anchor in the loop) + sox.debug(`linkedToFrom: link to current page (${currentPageId}) exists on question ${id}`); + $(this).append(''); + } else { + sox.debug(`linkedToFrom: link to current page not found on question ${id}`); + } + } else { + // If a link from 'linked questions' doesn't exist on the rest of the page + // Then it must be there _only_ due to the fact that the linked post references the current page + $(this).append(''); + } + }); }); }, @@ -1806,8 +1912,9 @@ // Description: Aligns badges by their class (bronze/silver/gold) on user profiles const acs = {}; + const $badges = $('.user-accounts tr .badges'); - $('.user-accounts tr .badges').each(function(i) { + $badges.each(function(i) { let b; let s; let g; if ($(this).find('>span[title*="bronze badge"]').length) { b = $(this).find('>span[title*="bronze badge"] .badgecount').text(); @@ -1824,8 +1931,8 @@ 'gold': g, }; }); - $.each(acs, (k) => { - const $badgesTd = $('.user-accounts tr .badges').eq(k); + $.each(acs, k => { + const $badgesTd = $badges.eq(k); $badgesTd.html(''); if (acs[k].gold) { $badgesTd.append('' + acs[k].gold + ''); @@ -1856,24 +1963,13 @@ function addLastSeen(userDetailsFromAPI) { $('.question, .answer, .reviewable-post').each(function() { - sox.debug('current post', $(this)); - const anchor = this.querySelector('.post-signature:last-child .user-details a[href^="/users"]'); - - if (!anchor) { - return; - } + if (!anchor) return; const id = sox.helpers.getIDFromAnchor(anchor); - sox.debug('quickAuthorInfo addLastSeen(): current id', id); - sox.debug('quickAuthorInfo addLastSeen(): userdetailscurrent id', userDetailsFromAPI[id]); - - if (!id || !(userDetailsFromAPI[id] && !this.getElementsByClassName('sox-last-seen').length)) { - return; - } + if (!id || !(userDetailsFromAPI[id] && !this.getElementsByClassName('sox-last-seen').length)) return; const lastSeenDate = new Date(userDetailsFromAPI[id].last_seen); - const type = userDetailsFromAPI[id].type === 'unregistered' ? ' (unregistered)' : ''; $(this).find('.user-info').last().append( @@ -1905,37 +2001,39 @@ } }); - sox.helpers.getFromAPI('users', Object.keys(postAuthors).join(';'), sox.site.currentApiParameter, FILTER_USER_LASTSEEN_TYPE, (data) => { - sox.debug('quickAuthorInfo api dump', data); - + sox.helpers.getFromAPI({ + endpoint: 'users', + ids: Object.keys(postAuthors), + sitename: sox.site.currentApiParameter, + filter: FILTER_USER_LASTSEEN_TYPE, + sort: 'creation', + featureId: 'quickAuthorInfo', + }, items => { const userDetailsFromAPI = {}; - data.items.forEach((user) => { + items.forEach(user => { userDetailsFromAPI[user.user_id] = { 'last_seen': user.last_access_date * 1000, 'type': user.user_type, }; }); - sox.debug('quickAuthorInfo userdetailsfromapi', userDetailsFromAPI); + sox.debug('quickAuthorInfo userDetailsFromAPI', userDetailsFromAPI); addLastSeen(userDetailsFromAPI); $(document).on('sox-new-comment', () => { //make sure it doesn't disappear when adding a new comment! addLastSeen(userDetailsFromAPI); }); - }, 'creation'); + }); } // key:id, value:username const postAuthors = {}; - sox.helpers.observe('.review-content', () => { + $(document).on('sox-new-review-post-appeared', () => { getIdsAndAddDetails(postAuthors); }); getIdsAndAddDetails(postAuthors); - - sox.debug('quickAuthorInfo answerer IDs', postAuthors); - sox.debug('quickAuthorInfo API call parameters', 'users', Object.keys(postAuthors).join(';'), sox.site.currentApiParameter); }, hiddenCommentsIndicator: function() { @@ -1944,28 +2042,24 @@ $('.question, .answer').each(function() { if ($(this).find('.js-show-link.comments-link:visible').length) { const postId = $(this).attr('data-questionid') || $(this).attr('data-answerid'); - const x = []; - const y = []; - const protocol = location.protocol; - const hostname = location.hostname; - const baseUrl = protocol + '//' + hostname; - $.get(baseUrl + '/posts/' + postId + '/comments', (d) => { + + $.get(baseUrl + '/posts/' + postId + '/comments', d => { const $commentCopy = $('#comments-' + postId + ' .comment-text .comment-copy'); $(d).find('.comment-text').each(function() { x.push($(this).find('.comment-copy').text()); }); - $commentCopy.filter((d) => { + $commentCopy.filter(d => { y.push(x.indexOf($commentCopy.eq(d).text())); }); - for (var i = 0; i < y.length; i++) { + for (let i = 0; i < y.length; i++) { if (y[i] != y[i + 1] - 1) { $commentCopy.filter(function() { return $(this).text() == x[y[i]]; @@ -2036,10 +2130,15 @@ this.addEventListener('mouseenter', function() { if (!this.dataset.tags) { - sox.helpers.getFromAPI('questions', id, sitename, FILTER_QUESTION_TAGS, (d) => { - this.dataset.tags = d.items[0].tags.join(', '); + sox.helpers.getFromAPI({ + endpoint: 'questions', + ids: id, + sitename, + filter: FILTER_QUESTION_TAGS, + useCache: false, // Single ID, so no point + }, items => { + this.dataset.tags = items[0].tags.join(', '); insertTagsList(this); - }); this.dataset.tags = PLACEHOLDER; } else if (typeof this.dataset.tags !== 'undefined' && this.dataset.tags !== PLACEHOLDER) { @@ -2065,7 +2164,7 @@ }); function checkAndAddReminder() { - if (!sox.user.loggedIn && !sox.location.on('winterbash2016.stackexchange.com')) { + if (!sox.user.loggedIn && !sox.location.match('winterbash')) { if (!$('#loggedInReminder').length) $('.container').append(div); } else { $('#loggedInReminder').remove(); @@ -2129,14 +2228,14 @@ $(this).next().find('.meta').hide().find('.newreply').remove(); }) - .on('click', '.newreply.added-by-sox', (e) => { + .on('click', '.newreply.added-by-sox', e => { const $message = $(e.target).closest('.message'); const id = $message.attr('id').split('-')[1]; const rest = $('#input').focus().val().replace(/^:([0-9]+)\s+/, ''); $('#input').val(':' + id + ' ' + rest).focus(); }); - var replySpan = $('', { + const replySpan = $('', { class: 'newreply added-by-sox', 'title': 'link my next chat message as a reply to this', }); @@ -2190,19 +2289,17 @@ // Description: Adds a coloured percentage bar above the pane on the right of the flag summary page to show percentage of helpful flags let helpfulFlags = 0; - $('td > a:contains(\'helpful\')').parent().prev().each(function() { - helpfulFlags += parseInt($(this).text().replace(',', '')); + $('li > a:contains(\'helpful\')').each(function() { + helpfulFlags += parseInt($(this).find('div:last').text().replace(',', '')); }); let declinedFlags = 0; - $('td > a:contains(\'declined\')').parent().prev().each(function() { - declinedFlags += parseInt($(this).text().replace(',', '')); + $('li > a:contains(\'declined\')').each(function() { + declinedFlags += parseInt($(this).find('div:last').text().replace(',', '')); }); if (helpfulFlags > 0) { - let percentHelpful = Number(Math.round((helpfulFlags / (helpfulFlags + declinedFlags)) * 100 + 'e2') + 'e-2'); - if (percentHelpful > 100) percentHelpful = 100; let percentColor; @@ -2215,12 +2312,12 @@ } //this is for the dynamic part; the rest of the CSS is in the main CSS file - GM_addStyle('#sox-flagPercentProgressBar:after {\ - background: ' + percentColor + ';\ - width: ' + percentHelpful + '%;\ - }'); + GM_addStyle(`#sox-flagPercentProgressBar:after { + background: ${percentColor}; + width: ${percentHelpful}%; + }`); - $('#flag-stat-info-table').before('

        ' + percentHelpful + '% helpful

        '); + $('#flag-summary-filter').after('

        ' + percentHelpful + '% helpful

        '); $('#sox-flagPercentHelpful span#percent').css('color', percentColor); $('#sox-flagPercentHelpful').after('
        '); @@ -2235,7 +2332,7 @@ const metaUrl = sox.Stack.options.site.childUrl; const requestUrl = proxyUrl + metaUrl + '/review'; - $.get(requestUrl, (d) => { + $.get(requestUrl, d => { const $doc = $(d); let total = 0; const $metaDashboardEl = $('.dashboard-item').last().find('.dashboard-count'); @@ -2285,9 +2382,11 @@ $(document).on('click', '.sox-copyCodeButton', function() { try { - if (!$('.sox-copyCodeTextarea').length) $('body').append('