NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name SW_Enhancer
// @version 0.94.3
// @author Nina Lanyon
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_log
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// @include https://similarworlds.com/*
// @updateURL https://openuserjs.org/meta/ninalanyon/SW_Enhancer.meta.js
// @downloadURL https://openuserjs.org/install/ninalanyon/SW_Enhancer.user.js
// @copyright 2021, Nina Lanyon (https://openuserjs.org/users/ninalanyon)
// @license MIT
// ==/UserScript==
/*
Changes:
- 0.94.3 Added let to for statements.
- 0.94.2 Removed some logging that was reading properties before they
existed.
- 0.94.1 SW implemented scrolling of the highlighted respons into
view so I have disabled my function.
- 0.93.4 SW changed the name of the visibility element.
- 0.93.3 SW changed the classname of active notifications causing
hiding of inactive notifications to fail. Fixed.
- 0.93.2 Fixed bug in hideUnreadableReplyAndThread which hides the
comments replying to a deleted comment.
- 0.93.1 Fixed bug in SetVisibility using window.eval to call and SW
function.
- 0.93.0 In progress: automatically follow people when responding.
- 0.92.0 Hide read mail in https://similarworlds.com/messages
- 0.90.3 Fixed the function that hides inactive notifications to hide
content box divs when all of the content is hidden.
- 0.90.1 Ignore mutations caused by the leechblock addon.
- 0.90 Added option to show only unread notifications
- 0.82 Added option to hide the Add downvote reaction button.
- 0.8 Fixed error in function that hides the motivational quotes, it
was hiding other elements as well.
- 0.7 First published version.
*/
/* ----------------
Define the configuration dialog.
*/
(async function () {
'use strict';
// A few bits of shorthand
function getElementById(id) {
return document.getElementById(id);
}
function getElementsByClassName(name) {
return document.getElementsByClassName(name);
}
function createElement(elementType) {
return document.createElement(elementType);
}
// Name some of the fields, field types, etc. to assist in
// preventing typos.
const SExpandCollapsedPosts = 'expandCollapsedPosts';
const SExpandCollapsedReplies = 'expandCollapsedReplies';
const SHideUnreadableRepliesAndThread = 'hideUnreadableRepliesAndThread';
const SHideUnwantedPostsInFeed = 'hideUnwantedPostsInFeed';
const SMatchUnwantedPostsInFeed = 'matchUnwantedPostsInFeed';
const SIgnoreCaseWhenMatching = "ignoreCaseWhenMatching";
const SHidePosQuotes = "hidePosQuotes";
const SEnableSWEnhancer = "enableSWEnhancer";
const SHideSWReactionsNeg = "hideSWReactionsNeg";
const SShowOnlyUnreadNotifications = "showOnlyUnreadNotifications";
const SShowOnlyUnreadMail = "showOnlyUnreadMail";
const SSetVisibility = "setVisibility";
const VVisibilityDoNotSet = "Do not set";
const VVisibilityLimited = "Limited";
const VVisibilityPublic = "Public";
const VVisibilityPrivate = "Private";
function makeConfiguration() {
console.log("a makeConfiguration");
const TCheckBox = "checkbox";
const TTextArea = "textarea";
const TRadio = "radio";
const KLabel = "label";
const KType = "type"
const KDefault = "default";
const KTitle = "title";
const KSection = "section";
const KRows = "rows";
const KCols = "cols";
const KOptions = "options";
console.log("SW m");
var fieldDefs = {
[SExpandCollapsedPosts]: {
[KSection]: ["Hiding and revealing parts of a post"],
[KLabel]: "Expand collapsed posts immediately",
[KTitle]: "Pictures in posts are hidden by default, ticking this box makes them show always.",
[KType]: TCheckBox,
[KDefault]: false
},
[SExpandCollapsedReplies]: {
[KLabel]: "Expand collapsed replies immediately",
[KTitle]: "Replies with sensitive content are collapsed by default on SW, ticking this box saves you the bother of clicking each one yourself.",
[KType]: TCheckBox,
[KDefault]: false
},
[SHideUnreadableRepliesAndThread]: {
[KLabel]: "Hide any replies that are marked as deleted together with all the replies to them",
[KType]: TCheckBox,
[KTitle]: "Once a reply has been deleted replies often become unfathomable and any you can't reply to them. Ticking this box hides the deleted item and all the replies to it.",
[KDefault]: false
},
[SHideUnwantedPostsInFeed]: {
[KSection]: ["Feed related settings"],
[KLabel]: 'Hide posts in the feed that match a regular expression',
[KType]: TCheckBox,
[KTitle]: "A simple example would be 'spank|diaper|poop' (without the quotation marks. This would hide any feed items that contain any of those words.",
[KDefault]: false
},
[SMatchUnwantedPostsInFeed]: {
[KLabel]: "Enter terms below separated by a pipe symbol: |. " +
"If any of these terms appear in post extracts or post titles in the feed then the post that contains them will be hidden. " +
"Note that this applies to the posts and the comments.",
[KType]: TTextArea,
[KTitle]: "A simple example would be 'spank|diaper|poop' (without the quotation marks. This would hide any feed items that contain any of those words.",
[KDefault]: "",
[KCols]: 60,
[KRows]: 4
},
[SIgnoreCaseWhenMatching]: {
[KLabel]: "Case insensitive matching.",
[KType]: TCheckBox,
[KTitle]: "For instance match all of these: Spank, SPANK, spank",
[KDefault]: true
},
[SEnableSWEnhancer]: {
[KSection]: ["Miscellaneous settings"],
[KLabel]: 'Enable this tool',
[KType]: TCheckBox,
[KTitle]: "Disabling still allows you to change the settings.",
[KDefault]: true
},
[SHidePosQuotes]: {
[KLabel]: 'Hide the silly motivational quotes',
[KType]: TCheckBox,
[KTitle]: "I find them irritating, I would rather that they weren't there.",
[KDefault]: false
},
[SHideSWReactionsNeg]: {
[KLabel]: 'Hide the Downvote button in the Add Reaction popup',
[KType]: TCheckBox,
[KTitle]: "Makes it impossible to accidentally downvote and removes the irritating thing from my sight!",
[KDefault]: true
},
[SShowOnlyUnreadNotifications]: {
[KLabel]: "Hide notifications that you have already clicked on.",
[KType]: TCheckBox,
[KTitle]: "Makes it harder to forget a notification.",
[KDefault]: true
},
[SShowOnlyUnreadMail]: {
[KLabel]: "Hide mail that you have already clicked on.",
[KType]: TCheckBox,
[KTitle]: "Makes it harder to forget a message.",
[KDefault]: true
},
[SSetVisibility]: {
[KLabel]: 'Post visibility',
[KType]: TRadio,
[KOptions]: [VVisibilityDoNotSet, VVisibilityPublic, VVisibilityLimited, VVisibilityPrivate],
[KTitle]: "When creating a new post or editing one set the visibility.",
[KDefault]: VVisibilityDoNotSet
}
};
GM_config.init({
id: 'SWEnhancerConfig',
title: 'SW Enhancer Settings',
fields: fieldDefs,
});
console.log("SW bp ");
//console.log("b " + GM_config.get(SExpandCollapsedPosts));
}
makeConfiguration();
console.log("cp ");
//console.log("c " + GM_config.get(SExpandCollapsedPosts));
/*try {
console.log("d " + GM_config.get(SExpandCollapsedReplies));
}
catch (error) {
console.error(error);
}
*/
function showConfiguration() {
GM_config.open();
}
console.log("SW F");
let menu = getElementById("sw-header-profile-drop");
if (menu != null) {
let sep = createElement("div");
sep.className = "header-dropsep";
menu.appendChild(sep);
console.log("SW G");
let item = createElement("div");
menu.appendChild(item);
let a = createElement("a");
a.className = "opt";
a.onclick = function () {
showConfiguration();
};
console.log("SW E ");
a.innerHTML = '<i class="sw-icon sw-i-edit-small"></i>User Settings';
item.appendChild(a);
}
// See
// https://plainjs.com/javascript/styles/get-the-position-of-an-element-relative-to-the-document-24/
function offset(el) {
const
rect = el.getBoundingClientRect(),
scrollLeft = window.pageXOffset || document.documentElement.scrollLeft,
scrollTop = window.pageYOffset || document.documentElement.scrollTop;
return {
left: rect.left + scrollLeft,
top: rect.top + scrollTop
};
}
var highlightedElementTop = 0;
var allowScroll = true;
var lastUrl = "";
var postEntryHeight = 0;
var cancelScrollTimerId = 0;
function cancelScroll() {
allowScroll = false;
}
// Return false if we should not scroll the highlighted comment
// into view. To determine this we check to see if the user has
// expanded the text areas for data entry.
function allowedToScroll() {
console.log("SW e allowedToScroll: ", allowScroll);
// SW has implemented this themselves so we should not do it as the two fight each other.
allowScroll = false
return allowScroll
if (lastUrl !== document.location.href) {
// SW sets the page url without 'navigating' to it. Make
// sure we start again if we are on a new page.
console.log("e url changed");
allowScroll = true;
postEntryHeight = 0;
lastUrl = document.location.href;
}
if (allowScroll) {
// Check to see if we have entered the text entry box. If
// so cancel scrolling.
const textEntries = getElementsByClassName("form-textarea");
const textEntry = textEntries.item(0);
const h = textEntry.clientHeight;
console.log("f: ", h);
if (postEntryHeight == 0) {
console.log("g peh was 0");
postEntryHeight = h;
}
if (postEntryHeight != h) {
console.log("h peh changed");
postEntryHeight = h;
allowScroll = false;
}
}
return allowScroll;
}
function scrollToHighlightedComment() {
if (!allowedToScroll()) {
return;
}
const divs = getElementsByClassName("cmt-hghlt");
console.log("i scrollToHighlightedComment: ", divs);
if (divs.length !== 0) {
let div = divs.item(0);
let rect = offset(div);
// Round to integer to avoid scrolling because of changes
// in the umpteenth decimal place.
let newTop = Math.round(rect.top);
console.log("j highlightedElementTop: ", highlightedElementTop);
console.log("k rect: ", rect.top);
if (newTop != highlightedElementTop) {
// position has changed.
highlightedElementTop = newTop;
divs.item(0).scrollIntoView({
behavior: 'smooth',
block: "end",
inline: "nearest"
});
clearTimeOut(cancelScrollTimeoutId);
cancelScrollimerId = setTimeout(cancelScroll, 1000)
}
}
}
function clickButtons(className) {
console.log("SW f");
const divs = getElementsByClassName(className);
for (let i = 0; i < divs.length; i++) {
divs.item(i).getElementsByClassName("small-button").item(0).click();
}
}
function unBlurInMessages() {
console.log("SW g");
const anchors = getElementsByClassName("small-button blur-btn active");
[].forEach.call(anchors, function (a) {
if (a.style.display != "none") {
a.click();
}
})
}
function clickStoryCompressed() {
console.log("SW h");
const a = getElementById("story-compressed");
if (a !== null) {
a.click();
}
}
const rootUrl = "https://similarworlds.com/";
const feedsUrls = [rootUrl,
rootUrl + "top",
rootUrl + "new"
];
function getParent(element, rel) {
if (rel === 0) {
return element;
}
else {
return getParent(element.parentElement, rel - 1);
}
}
function hideContentBoxIfContentsHidden(contentBox) {
console.log("i");
let active =
contentBox.getElementsByClassName("sw-notif-active");
if (active.length == 0) {
contentBox.style.display = "none";
}
}
function hideItemsRelative(elements, parentRel) {
"use strict";
console.log("k");
let contentBoxes = new Set();
try {
for (let i = 0; i < elements.length; i++) {
let element = elements[i];
let parent = getParent(element, parentRel);
parent.style.display = "none";
contentBoxes.add(parent.parentElement);
}
}
catch (ex) {
alert('b ex: ' + ex.toString());
}
contentBoxes.forEach(hideContentBoxIfContentsHidden);
}
function hideItems(className, parentRel, matcher) {
"use strict";
console.log("SW j");
var elements = document.getElementsByClassName(className),
element,
text,
i;
try {
for (let i = 0; i < elements.length; i++) {
element = elements[i];
if (element) {
text = element.textContent;
if (typeof text !== 'undefined') {
if (text.search(matcher) !== -1) {
let parent = getParent(element, parentRel);
parent.style.display = "none";
//parent.visibility = "hidden";
//element.style.display = "none";
//element.visibility = "hidden";
}
}
}
}
}
catch (ex) {
alert('b ex: ' + ex.toString());
}
}
function hideUnwantedPostsInFeed() {
let matchSrc = GM_config.get(SMatchUnwantedPostsInFeed);
let matchInsensitive = GM_config.get(SIgnoreCaseWhenMatching);
let flags = matchInsensitive ? "i" : "";
let matcher = new RegExp(matchSrc, flags);
hideItems("swcmnt", 3, matcher);
hideItems("cmnt-post-prevw", 3, matcher);
hideItems("pstbx-group", 2, matcher);
hideItems("rctrpl-comment", 3, matcher);
}
function runClickButtons() {
// click the view more replies button
console.log("d ");
//alert("runClickButtons");
if (GM_config.get(SExpandCollapsedReplies)) {
clickButtons("nested-comments-getmore");
}
clickButtons("cmtbx-showhdn");
unBlurInMessages();
console.log("e ");
if (GM_config.get(SExpandCollapsedPosts)) {
clickStoryCompressed();
}
// TODO: reinstate when fixed:
//scrollToHighlightedComment();
}
function hideClass(className) {
"use strict";
var elements = document.getElementsByClassName(className);
console.log("SW l " + className + " " + elements.length);
for (var i = 0; i < elements.length; i++) {
let element = elements[i];
element.style.display = "none";
}
}
function hidePosQuotes() {
"use strict";
console.log("SW p hidePosQuotes");
hideClass("pos-quote");
hideClass("sw-body-1-aa");
hideClass("pos-quote-in");
//hideClass("cmtbx-bx-body");
//hideClass("cmtbx-elem");
let e = getElementById("sw-right-side-aa");
e.style.display = "none";
}
function hideSWReactionsNeg() {
hideClass("sw-reactions neg");
}
function hideOldNotifications() {
let active = getElementsByClassName("sw-notif active");
if (active.length == 0) {
// No active items. So we should not hide the inactive ones either.
return;
}
let inactive = document.querySelectorAll('.sw-notif:not(.active)');
hideItemsRelative(inactive, 0);
}
function hideOldMail() {
let active = document.querySelectorAll('.message-box:not(.read)');
if (active.length == 0) {
// No active items. So we should not hide the inactive ones either.
return;
}
hideClass('message-box read');
}
function hideUnreadableReplyAndThread(element) {
"use strict";
element.style.display = "none";
let next = element.nextElementSibling;
if (next == null) {
return;
}
next.style.display = "none";
/*let comslst = next.firstElementChild;
let firstComment = comslst.firstElementChild;
if (firstComment.classList.contains("cmt-nst")){
comslst.style.display = "none";
}
*/
}
function hideUnreadableRepliesAndThread() {
"use strict";
var elements = document.getElementsByClassName("comment-deleted");
console.log("SW sa " + elements.length);
for (var i = 0; i < elements.length; i++) {
hideUnreadableReplyAndThread(elements[i]);
}
}
// function setVisibility(){
// let postseg_3 = getElementById("postseg-3");
// if (postseg_3 == null || postseg_3.style.display == "none"){
// // Not ready to be set yet or not on the right page.
// return;
// }
// let newVisibility = GM_config.get(SSetVisibility);
// let code = [VVisibilityDoNotSet,
// VVisibilityPublic,
// VVisibilityPrivate,
// VVisibilityLimited].indexOf(newVisibility);
// if (code == 0){
// return;
// }
// let visibilitySpan = getElementById("postentry-visible" + code);
// if (visibilitySpan.style.display == "none"){
// window.eval("postEditVisibility('0', " + code + ", 'postentry-visible');");
// }
// }
function setVisibility() {
var visibilitySpan;
for (let code = 1; code <= 3; code++) {
visibilitySpan = getElementById("postentry-visible" + code);
if (visibilitySpan != null && visibilitySpan.style.display != "none") {
break;
}
}
if (visibilitySpan == null || visibilitySpan.style.display == "none") {
// Not ready to be set yet or not on the right page.
return;
}
let newVisibility = GM_config.get(SSetVisibility);
let code = [VVisibilityDoNotSet,
VVisibilityPublic,
VVisibilityPrivate,
VVisibilityLimited
].indexOf(newVisibility);
if (code == 0) {
return;
}
visibilitySpan = getElementById("postentry-visible" + code);
if (visibilitySpan.style.display == "none") {
window.eval("postEditVisibility('0', " + code + ", 'postentry-visible');");
}
}
function runFixUp() {
console.log("SW q runFixUp");
//alert("SW a " + window.location.href);
if (feedsUrls.includes(window.location.href)) {
if (GM_config.get(SHideUnwantedPostsInFeed)) {
hideUnwantedPostsInFeed();
}
}
else {
runClickButtons();
}
if (GM_config.get(SHidePosQuotes, false)) {
hidePosQuotes();
}
if (GM_config.get(SHideSWReactionsNeg, true)) {
hideSWReactionsNeg();
}
if (window.location.href === "https://similarworlds.com/notifications" &&
GM_config.get(SShowOnlyUnreadNotifications, true)) {
hideOldNotifications();
}
if (window.location.href === "https://similarworlds.com/messages" &&
GM_config.get(SShowOnlyUnreadMail, true)) {
hideOldMail();
}
if (GM_config.get(SHideUnreadableRepliesAndThread, true)) {
hideUnreadableRepliesAndThread();
}
setVisibility();
}
function run() {
console.log("SW n run");
//alert("SW B");
let timerId = 0;
const observer = new MutationObserver((mutationList, observer) => {
if (mutationList.length == 1 &&
mutationList[0].target.className == "leechblock-timer") {
// Leech block places a countdown in the top left
// corner which causes mutations that we aren't
// interested in, just ignore them.
return;
}
console.log("SW o observer", mutationList);
// cancel the timeout if it is running so that we don't run
// until the cascade of mutations has died down.
clearTimeout(timerId);
// click the buttons a little while after the last mutation event
timerId = setTimeout(runFixUp, 100)
});
// observe everything
observer.observe(
document.body, {
childList: true, // observe direct children
subtree: true, // and lower descendants too
attributes: true
}
);
}
console.log("SW D");
//alert("SW D ");
//if (GM_config.get(SEnableSWEnhancer)) {
if (true) {
console.log("SW l");
//alert("SW C");
run();
}
})()