Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
e53c223
initial commit
marcustyphoon Jul 27, 2022
696808d
fix invisible warning
marcustyphoon Aug 3, 2022
e3f52e4
redesign warning element state logic
marcustyphoon Aug 3, 2022
26560e9
my long state machine rewrite is perfectly constructed
marcustyphoon Aug 4, 2022
cef5fc2
cleanup
marcustyphoon Aug 4, 2022
a5b1a2e
Clear dismissed warnings in clean()
marcustyphoon Aug 4, 2022
7e106e3
Implement dataset-based non css method
marcustyphoon Aug 4, 2022
881f554
Get timeline UUIDs using timelineObject
marcustyphoon Aug 17, 2022
f3c4956
Fix options display with no muted blogs
marcustyphoon Aug 18, 2022
6cae4f2
Revamp storage keys
marcustyphoon Aug 18, 2022
45c8a33
Make styling slightly less terrible
marcustyphoon Aug 18, 2022
0d92caf
Fix async race condition
marcustyphoon Aug 18, 2022
0acce37
Merge branch 'master' into mute
marcustyphoon Sep 2, 2022
66ae4b3
Merge branch 'master' into mute
marcustyphoon Dec 7, 2022
79de138
exclude single post blog view
marcustyphoon Dec 12, 2022
991f4f8
rewrite on-blog warnings (works on all modes now)
marcustyphoon Dec 12, 2022
f545fa4
simplify onstoragechanged
marcustyphoon Dec 12, 2022
9bef00b
remove persistent unmuting
marcustyphoon Dec 12, 2022
2ff9c31
remove test/temporary code
marcustyphoon Dec 12, 2022
50a3098
optionally check the reblog trail
marcustyphoon Dec 12, 2022
9bbabc5
uhhh oops
marcustyphoon Dec 12, 2022
b85bb2e
fix random ads appearing when all posts hidden
marcustyphoon Dec 13, 2022
ea05c44
clarify meatballs menu
marcustyphoon Dec 14, 2022
83e649f
description that I dislike but it's something
marcustyphoon Dec 14, 2022
56f6e67
Merge branch 'master' into mute
marcustyphoon Jan 29, 2023
467eee6
fix warning element removal
marcustyphoon Feb 16, 2023
3530c9b
UI clarity tweaks; refactors
marcustyphoon Feb 16, 2023
a139f78
handle contributed content
marcustyphoon Feb 26, 2023
5e5ee8b
improve warning css
marcustyphoon Mar 7, 2023
562f53a
fix blog view exclusion logic
marcustyphoon Mar 15, 2023
70b852a
Merge remote-tracking branch 'upstream/master' into mute
marcustyphoon Mar 23, 2023
da4565e
customize meatballs menu label
marcustyphoon Mar 23, 2023
22f1b6e
Merge branch 'master' into mute
marcustyphoon Apr 30, 2023
432f846
enable mute menu in blog meatball menus
marcustyphoon May 1, 2023
1cacee5
Merge branch 'master' into mute
marcustyphoon May 5, 2023
fb8e016
combine storage set calls
marcustyphoon Aug 12, 2023
191a195
Merge remote-tracking branch 'upstream/master' into mute
marcustyphoon Aug 17, 2023
792d52d
Update mute.js
marcustyphoon Aug 17, 2023
8462228
Merge branch 'master' into mute
marcustyphoon Oct 27, 2023
6f3d7e7
fix mute dom child
marcustyphoon Feb 11, 2024
0ccb074
fix warning element DOM position with virtual scroller
marcustyphoon Mar 21, 2024
eeab105
Use CSS for placeholder text state
marcustyphoon Apr 29, 2024
5f6a294
refactor list item replacement
marcustyphoon Apr 29, 2024
8337f1e
refactor buttons
marcustyphoon Apr 29, 2024
6f075a3
prettier
marcustyphoon Apr 29, 2024
e0af289
refactor conditional modal
marcustyphoon Apr 29, 2024
1537140
Merge branch 'master' into mute
marcustyphoon Jun 22, 2024
edad23a
correctly parse communities post authors
marcustyphoon Jun 22, 2024
24c6515
partial data-timeline-id implementation
marcustyphoon Jun 22, 2024
0ad3757
Revert "partial data-timeline-id implementation"
marcustyphoon Jun 22, 2024
4baa9d0
Merge branch 'master' into mute
marcustyphoon Jun 23, 2024
6b4fd92
Merge branch 'master' into mute
marcustyphoon Jun 23, 2024
05c4cff
remove missed logging
marcustyphoon Sep 19, 2024
f8e8b8e
Merge branch 'master' into mute
marcustyphoon Sep 19, 2024
8b9a621
fix loop logic bug
marcustyphoon Sep 24, 2024
2586d34
fix (kinda) ui colors on patio
marcustyphoon Sep 24, 2024
87a674b
timeline id util: add likes
marcustyphoon Sep 24, 2024
3d9ced8
timeline id util: add single post view
marcustyphoon Sep 24, 2024
2cb086d
timeline id util: add channelselector
marcustyphoon Sep 24, 2024
0fa274a
implement data-timeline-id
marcustyphoon Sep 24, 2024
d3c48b1
arrow parens
marcustyphoon Sep 25, 2024
87c4626
various clarifying refactors
marcustyphoon Sep 25, 2024
a1e7f76
remove flicker on mute/unmute
marcustyphoon Sep 25, 2024
4710041
timeline id util: fix single post view
marcustyphoon Sep 26, 2024
dcf4be6
fix muted blog timelines with endless scrolling disabled
marcustyphoon Sep 26, 2024
262c76a
technically this will matter in november 2303.
marcustyphoon Sep 26, 2024
15b2f85
Merge branch 'master' into mute
marcustyphoon Jan 7, 2025
1757628
update capitalization of "onclick"
marcustyphoon Jan 7, 2025
82af16e
various refactors
marcustyphoon Feb 6, 2025
3193e1d
refactors for clarification
marcustyphoon Feb 6, 2025
85c4e8a
disable on more timelines
marcustyphoon Feb 6, 2025
3a22cd2
process peepr search/tagged correctly
marcustyphoon Feb 6, 2025
19185f2
disable on own public likes
marcustyphoon Feb 6, 2025
77a3d67
fix list/masonry switch removing class
marcustyphoon Feb 6, 2025
9a5c52d
remove unused export
marcustyphoon Feb 6, 2025
1255de1
Merge branch 'master' into mute
marcustyphoon Mar 30, 2025
5c32fd3
apply #1544
marcustyphoon Mar 30, 2025
c4a9b74
Merge remote-tracking branch 'upstream/master' into mute
marcustyphoon Jun 30, 2025
4cb7286
update directory structure
marcustyphoon Jun 30, 2025
916a5e4
Merge remote-tracking branch 'upstream/master' into mute
marcustyphoon Oct 17, 2025
823bf29
use new dom utils
marcustyphoon Oct 17, 2025
7c11847
Merge remote-tracking branch 'upstream/master' into mute
marcustyphoon Oct 22, 2025
8ed8b4c
use more new dom utils
marcustyphoon Oct 22, 2025
2842166
Merge branch 'master' into mute
marcustyphoon Oct 25, 2025
b4bfc33
Merge remote-tracking branch 'upstream/master' into mute
marcustyphoon Nov 12, 2025
bb6240f
consolidate duplicates of updated `blogTimelineFilter`
marcustyphoon Nov 12, 2025
9469fa2
update anyPostPermalinkTimelineFilter
marcustyphoon Nov 12, 2025
52e9298
Merge remote-tracking branch 'upstream/master' into mute
marcustyphoon Feb 3, 2026
48b9cf8
format
marcustyphoon Feb 3, 2026
4b124a2
Merge remote-tracking branch 'upstream/master' into mute
marcustyphoon Feb 14, 2026
8671173
organize imports
marcustyphoon Feb 14, 2026
75f3744
Merge remote-tracking branch 'upstream/master' into mute
marcustyphoon Feb 28, 2026
8bc9455
fix timeline id util
marcustyphoon Mar 6, 2026
281ea26
Merge branch 'master' into mute
marcustyphoon Apr 30, 2026
90ccc43
update timeline id util
marcustyphoon Apr 30, 2026
1b1ac58
basic component preference migration
marcustyphoon May 4, 2026
acbec2e
add unmute confirmation dialogue
marcustyphoon May 17, 2026
b87f3b0
fix mode selector
marcustyphoon May 17, 2026
aeb21b1
tweak unmute confirmation wording? idk
marcustyphoon May 17, 2026
586df38
Merge branch 'master' into mute
marcustyphoon Jun 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/features/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"mass_privater",
"mass_unliker",
"mirror_posts",
"mute",
"mutual_checker",
"no_recommended",
"notificationblock",
Expand Down
26 changes: 26 additions & 0 deletions src/features/mute/feature.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"title": "Mute",
"description": "Hide users' posts all over Tumblr",
"icon": {
"class_name": "ri-volume-mute-line",
"color": "white",
"background_color": "#C20044"
},
"relatedTerms": [ "Blacklist", "Filter", "Savior" ],
"preferences": {
"manageBlockedPosts": {
"type": "component",
"src": "/features/mute/options/index.js"
},
"checkTrail": {
"type": "checkbox",
"label": "Hide posts with a muted user anywhere in the trail",
"default": false
},
"contributedContentOriginal": {
"type": "checkbox",
"label": "Treat reblogs with contributed content as original",
"default": false
}
}
}
33 changes: 33 additions & 0 deletions src/features/mute/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[data-mute-active] [data-mute-hidden],
[data-muted-blog-controls-mode='original'] ~ div [data-muted-blog-controls-hidden],
[data-muted-blog-controls-mode='reblogged'] ~ div [data-muted-blog-controls-hidden] {
content: linear-gradient(transparent, transparent);
height: 0;
}

/* prevents endless post loading by preserving post height */
[data-muted-blog-controls-mode='all'] ~ div article,
[data-muted-blog-controls-mode='all'] ~ div article :is(img, video, canvas) {
visibility: hidden !important;
}

.xkit-mute-lengthened {
min-height: 100vh;
}

.xkit-muted-blog-controls {
padding: 25px 20px;
border-radius: 3px;
margin-bottom: var(--post-padding);

background-color: var(--blog-title-color-15, rgba(var(--white-on-dark), 0.25));
color: var(--blog-title-color, rgba(var(--white-on-dark)));

font-weight: 700;
text-align: center;
line-height: 1.5em;
}

.xkit-muted-blog-controls button {
color: var(--blog-link-color, rgb(var(--deprecated-accent)));
}
304 changes: 304 additions & 0 deletions src/features/mute/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
import { keyToCss } from '../../utils/css_map.js';
import { br, button, div, form, input, label } from '../../utils/dom.js';
import { filterPostElements, getTimelineItemWrapper, postSelector } from '../../utils/interface.js';
import { registerBlogMeatballItem, registerMeatballItem, unregisterBlogMeatballItem, unregisterMeatballItem } from '../../utils/meatballs.js';
import { hideModal, modalCancelButton, showModal } from '../../utils/modals.js';
import { onNewPosts, pageModifications } from '../../utils/mutations.js';
import { getPreferences } from '../../utils/preferences.js';
import { timelineObject } from '../../utils/react_props.js';
import {
anyBlogTimelineFilter,
anyFlaggedReviewTimelineFilter,
anyPostPermalinkTimelineFilter,
blogTimelineFilter,
inboxTimelineFilter,
likesTimelineFilter,
peeprLikesTimelineFilter,
timelineSelector,
} from '../../utils/timeline_id.js';
import { userBlogNames } from '../../utils/user.js';
import { controlsClass as showOriginalsControlsClass } from '../show_originals/index.js';

const meatballButtonId = 'mute';
const meatballButtonLabel = data => `Mute options for ${data.name ?? getVisibleBlog(data).name}`;

const hiddenAttribute = 'data-mute-hidden';
const mutedBlogControlsHiddenAttribute = 'data-muted-blog-controls-hidden';
const activeAttribute = 'data-mute-active';
const mutedBlogControlsClass = 'xkit-muted-blog-controls';
const lengthenedClass = 'xkit-mute-lengthened';

const blogNamesStorageKey = 'mute.blogNames';
const mutedBlogEntriesStorageKey = 'mute.mutedBlogEntries';

let checkTrail;
let contributedContentOriginal;

let blogNames = {};
let mutedBlogs = {};

const lengthenTimeline = timeline => {
if (!timeline.querySelector(keyToCss('manualPaginatorButtons'))) {
timeline.classList.add(lengthenedClass);
}
};

// Attempts to get the blog name and blog UUID of a timeline element if it contains the posts from a single blog.
// The element itself doesn't contain both values, so a post object must be found with the required data.
const getNameAndUuid = async timelineElement => {
for (const post of [...timelineElement.querySelectorAll(postSelector)]) {
const { blog: { name, uuid } } = await timelineObject(post);
if (
blogTimelineFilter(name)(timelineElement) ||
blogTimelineFilter(uuid)(timelineElement)
) {
return { name, uuid };
}
}
throw new Error('could not determine blog name / UUID for timeline element:', timelineElement);
};

const processBlogTimelineElement = async timelineElement => {
const { name, uuid } = await getNameAndUuid(timelineElement);
const mutedBlogMode = mutedBlogs[uuid];

if (mutedBlogMode) {
timelineElement.dataset.muteBlogUuid = uuid;

const mutedBlogControls = div({ class: mutedBlogControlsClass, 'data-muted-blog-controls-mode': mutedBlogMode }, [
`You have muted ${mutedBlogMode} posts from ${name}!`,
br(),
button({ click: () => mutedBlogControls.remove() }, ['show posts anyway']),
]);
timelineElement.prepend(mutedBlogControls);
timelineElement.querySelector(`.${showOriginalsControlsClass}`)?.after(mutedBlogControls);
}
};

const shouldDisable = timelineElement => Boolean(
userBlogNames.some(name => blogTimelineFilter(name)(timelineElement)) ||
anyFlaggedReviewTimelineFilter(timelineElement) ||
likesTimelineFilter(timelineElement) ||
userBlogNames.some(name => peeprLikesTimelineFilter(name)(timelineElement)) ||
inboxTimelineFilter(timelineElement) ||
anyPostPermalinkTimelineFilter(timelineElement),
);

const processTimelines = async timelineElements => {
for (const timelineElement of [...new Set(timelineElements)]) {
const { timeline, timelineId, muteProcessedTimeline, muteProcessedTimelineId } = timelineElement.dataset;

const alreadyProcessed =
(timeline && timeline === muteProcessedTimeline) ||
(timelineId && timelineId === muteProcessedTimelineId);
if (alreadyProcessed) continue;

timelineElement.dataset.muteProcessedTimeline = timeline;
timelineElement.dataset.muteProcessedTimelineId = timelineId;

[...timelineElement.querySelectorAll(`.${mutedBlogControlsClass}`)].forEach(el => el.remove());
delete timelineElement.dataset.muteBlogUuid;
timelineElement.removeAttribute(activeAttribute);

if (shouldDisable(timelineElement) === false) {
timelineElement.setAttribute(activeAttribute, '');
lengthenTimeline(timelineElement);

if (anyBlogTimelineFilter(timelineElement)) {
await processBlogTimelineElement(timelineElement).catch(console.log);
}
}
}
};

const updateStoredName = (uuid, name) => {
blogNames[uuid] = name;
Object.keys(blogNames).forEach(uuid => {
if (!mutedBlogs[uuid]) {
delete blogNames[uuid];
}
});
browser.storage.local.set({ [blogNamesStorageKey]: blogNames });
};

const getVisibleBlog = ({ blog, authorBlog, community }) => (community ? authorBlog : blog);

const processPosts = async function (postElements) {
await processTimelines(postElements.map(postElement => postElement.closest(timelineSelector)));

filterPostElements(postElements, { includeFiltered: true }).forEach(async postElement => {
const timelineObjectData = await timelineObject(postElement);
const { uuid, name } = getVisibleBlog(timelineObjectData);
const { rebloggedRootUuid, content = [], trail = [] } = timelineObjectData;

const { muteBlogUuid: timelineBlogUuid } = postElement.closest(timelineSelector).dataset;

if (mutedBlogs[uuid] && blogNames[uuid] !== name) {
updateStoredName(uuid, name);
}

const hidePost = relevantBlogUuid =>
getTimelineItemWrapper(postElement).setAttribute(
// Posts hidden on blog timelines can be revealed by muted blog timeline controls
// if and only if they are hidden because the current blog is muted.
relevantBlogUuid === timelineBlogUuid
? mutedBlogControlsHiddenAttribute
: hiddenAttribute,
'',
);

const isRebloggedPost = contributedContentOriginal
? rebloggedRootUuid && !content.length
: rebloggedRootUuid;

const originalUuid = isRebloggedPost ? rebloggedRootUuid : uuid;
const reblogUuid = isRebloggedPost ? uuid : null;

if (['all', 'original'].includes(mutedBlogs[originalUuid])) {
hidePost(originalUuid);
}
if (['all', 'reblogged'].includes(mutedBlogs[reblogUuid])) {
hidePost(reblogUuid);
}

if (checkTrail) {
for (const { blog } of trail) {
if (['all'].includes(mutedBlogs[blog?.uuid])) {
hidePost(blog.uuid);
}
}
}
});
};

const onMeatballButtonClicked = function ({ currentTarget }) {
const { name, uuid } = currentTarget.__timelineObjectData
? getVisibleBlog(currentTarget.__timelineObjectData)
: currentTarget.__blogData;

const currentMode = mutedBlogs[uuid];

const createRadioElement = value =>
label({}, [
`Hide ${value} posts`,
input({ type: 'radio', name: 'muteOption', value }),
]);

const formElement = form(
{ id: 'xkit-mute-form', 'data-name': name, 'data-uuid': uuid, submit: muteUser },
[
createRadioElement('all'),
createRadioElement('original'),
createRadioElement('reblogged'),
],
);

formElement.elements.muteOption.value = currentMode;

currentMode
? showModal({
title: `Mute options for ${name}:`,
message: [formElement],
buttons: [
modalCancelButton,
button({ class: 'blue', click: () => unmuteUser(uuid) }, ['Unmute']),
input({ type: 'submit', form: formElement.id, class: 'red', value: 'Update Mode' }),
],
})
: showModal({
title: `Mute ${name}?`,
message: [formElement],
buttons: [
modalCancelButton,
input({ type: 'submit', form: formElement.id, class: 'red', value: 'Mute' }),
],
});
};

const muteUser = event => {
event.preventDefault();

const { name, uuid } = event.currentTarget.dataset;
const { value } = event.currentTarget.elements.muteOption;
if (value === '') return;

mutedBlogs[uuid] = value;
blogNames[uuid] = name;

browser.storage.local.set({
[mutedBlogEntriesStorageKey]: Object.entries(mutedBlogs),
[blogNamesStorageKey]: blogNames,
});

hideModal();
};

const unmuteUser = uuid => {
delete mutedBlogs[uuid];
browser.storage.local.set({ [mutedBlogEntriesStorageKey]: Object.entries(mutedBlogs) });

hideModal();
};

export const onStorageChanged = async function (changes, areaName) {
const {
[blogNamesStorageKey]: blogNamesChanges,
[mutedBlogEntriesStorageKey]: mutedBlogsEntriesChanges,
} = changes;

if (Object.keys(changes).some(key => key.startsWith('mute.preferences') && changes[key].oldValue !== undefined)) {
clean().then(main);
return;
}

if (blogNamesChanges) {
({ newValue: blogNames } = blogNamesChanges);
}

if (mutedBlogsEntriesChanges) {
const { newValue: mutedBlogsEntries } = mutedBlogsEntriesChanges;
mutedBlogs = Object.fromEntries(mutedBlogsEntries ?? []);

unprocess();
pageModifications.trigger(processPosts);
}
};

export const main = async function () {
({ checkTrail, contributedContentOriginal } = await getPreferences('mute'));
({ [blogNamesStorageKey]: blogNames = {} } = await browser.storage.local.get(blogNamesStorageKey));
const { [mutedBlogEntriesStorageKey]: mutedBlogsEntries } = await browser.storage.local.get(mutedBlogEntriesStorageKey);
mutedBlogs = Object.fromEntries(mutedBlogsEntries ?? []);

registerMeatballItem({
id: meatballButtonId,
label: meatballButtonLabel,
onclick: onMeatballButtonClicked,
});
registerBlogMeatballItem({
id: meatballButtonId,
label: meatballButtonLabel,
onclick: onMeatballButtonClicked,
});
onNewPosts.addListener(processPosts);
};

const unprocess = () => {
$(`[${hiddenAttribute}]`).removeAttr(hiddenAttribute);
$(`[${mutedBlogControlsHiddenAttribute}]`).removeAttr(mutedBlogControlsHiddenAttribute);
$(`[${activeAttribute}]`).removeAttr(activeAttribute);
$(`.${lengthenedClass}`).removeClass(lengthenedClass);
$(`.${mutedBlogControlsClass}`).remove();
$('[data-mute-processed-timeline]').removeAttr('data-mute-processed-timeline');
$('[data-mute-processed-timeline-id]').removeAttr('data-mute-processed-timeline-id');
$('[data-mute-blog-uuid]').removeAttr('data-mute-blog-uuid');
};

export const clean = async function () {
unprocess();
unregisterMeatballItem(meatballButtonId);
unregisterBlogMeatballItem(meatballButtonId);
onNewPosts.removeListener(processPosts);
};

export const stylesheet = true;
Loading