diff --git a/src/css/style.css b/src/css/style.css
index 8ef057ad..3dac0e62 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -6137,6 +6137,17 @@ emoji-picker-dialog emoji-picker,
flex: none;
}
+.profile-reposts-hidden {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.profile-reposts-hidden app-icon {
+ width: 14px;
+ height: 14px;
+}
+
.profile-blocked-badge {
font-size: 13px;
background: var(--secondary-button-color);
diff --git a/src/img/icons/custom/repost-off.svg b/src/img/icons/custom/repost-off.svg
new file mode 100644
index 00000000..57dd1592
--- /dev/null
+++ b/src/img/icons/custom/repost-off.svg
@@ -0,0 +1 @@
+
diff --git a/src/js/api.js b/src/js/api.js
index fb930ae2..da182c7d 100644
--- a/src/js/api.js
+++ b/src/js/api.js
@@ -1118,6 +1118,18 @@ export class Api {
return res;
}
+ async muteActorReposts(did) {
+ const res = await this.appViewRequest("app.bsky.graph.muteActor", {
+ method: "POST",
+ body: {
+ actor: did,
+ onlyReposts: true,
+ },
+ parseJson: false,
+ });
+ return res;
+ }
+
async unmuteActor(did) {
const res = await this.appViewRequest("app.bsky.graph.unmuteActor", {
method: "POST",
diff --git a/src/js/dataLayer/derived.js b/src/js/dataLayer/derived.js
index 50592b80..92dc561f 100644
--- a/src/js/dataLayer/derived.js
+++ b/src/js/dataLayer/derived.js
@@ -167,6 +167,8 @@ export class Derived extends ReactiveStore {
this.patchStore.hasPendingProfilePatch(did, [
"muteProfile",
"unmuteProfile",
+ "muteProfileReposts",
+ "unmuteProfileReposts",
]),
);
this.$patchedPosts = new ComputedMap((uri) => {
diff --git a/src/js/dataLayer/mutations.js b/src/js/dataLayer/mutations.js
index 79e80227..79e8e5e3 100644
--- a/src/js/dataLayer/mutations.js
+++ b/src/js/dataLayer/mutations.js
@@ -775,17 +775,8 @@ export class Mutations {
await this.api.muteActor(profile.did);
this._updateStoredProfile(profile, (stored) => ({
...stored,
- viewer: { ...stored.viewer, muted: true },
+ viewer: { ...stored.viewer, muted: true, mutedOnlyReposts: false },
}));
- this._updatePostsByAuthor(profile.did, (post) => {
- return {
- ...post,
- author: {
- ...post.author,
- viewer: { ...post.author.viewer, muted: true },
- },
- };
- });
const mutedProfiles = this.dataStore.$mutedProfiles.get();
if (mutedProfiles) {
const alreadyListed = mutedProfiles.mutes.some(
@@ -814,17 +805,8 @@ export class Mutations {
await this.api.unmuteActor(profile.did);
this._updateStoredProfile(profile, (stored) => ({
...stored,
- viewer: { ...stored.viewer, muted: false },
+ viewer: { ...stored.viewer, muted: false, mutedOnlyReposts: false },
}));
- this._updatePostsByAuthor(profile.did, (post) => {
- return {
- ...post,
- author: {
- ...post.author,
- viewer: { ...post.author.viewer, muted: false },
- },
- };
- });
const mutedProfiles = this.dataStore.$mutedProfiles.get();
if (mutedProfiles) {
this.dataStore.$mutedProfiles.set({
@@ -842,6 +824,42 @@ export class Mutations {
}
}
+ async muteProfileReposts(profile) {
+ const patchId = this.patchStore.addProfilePatch(profile.did, {
+ type: "muteProfileReposts",
+ });
+ try {
+ await this.api.muteActorReposts(profile.did);
+ this._updateStoredProfile(profile, (stored) => ({
+ ...stored,
+ viewer: { ...stored.viewer, mutedOnlyReposts: true },
+ }));
+ } catch (error) {
+ console.error(error);
+ throw error;
+ } finally {
+ this.patchStore.removeProfilePatch(profile.did, patchId);
+ }
+ }
+
+ async unmuteProfileReposts(profile) {
+ const patchId = this.patchStore.addProfilePatch(profile.did, {
+ type: "unmuteProfileReposts",
+ });
+ try {
+ await this.api.unmuteActor(profile.did);
+ this._updateStoredProfile(profile, (stored) => ({
+ ...stored,
+ viewer: { ...stored.viewer, mutedOnlyReposts: false },
+ }));
+ } catch (error) {
+ console.error(error);
+ throw error;
+ } finally {
+ this.patchStore.removeProfilePatch(profile.did, patchId);
+ }
+ }
+
async blockProfile(profile) {
const patchId = this.patchStore.addProfilePatch(profile.did, {
type: "blockProfile",
@@ -852,15 +870,6 @@ export class Mutations {
...stored,
viewer: { ...stored.viewer, blocking: block.uri },
}));
- this._updatePostsByAuthor(profile.did, (post) => {
- return {
- ...post,
- author: {
- ...post.author,
- viewer: { ...post.author.viewer, blocking: block.uri },
- },
- };
- });
const blockedProfiles = this.dataStore.$blockedProfiles.get();
if (blockedProfiles) {
const alreadyListed = blockedProfiles.blocks.some(
@@ -910,15 +919,6 @@ export class Mutations {
...stored,
viewer: { ...stored.viewer, blocking: null },
}));
- this._updatePostsByAuthor(profile.did, (post) => {
- return {
- ...post,
- author: {
- ...post.author,
- viewer: { ...post.author.viewer, blocking: null },
- },
- };
- });
const blockedProfiles = this.dataStore.$blockedProfiles.get();
if (blockedProfiles) {
this.dataStore.$blockedProfiles.set({
@@ -2358,14 +2358,6 @@ export class Mutations {
}
}
- _updatePostsByAuthor(profileDid, updateFunc) {
- for (const post of this.dataStore.$posts.values()) {
- if (post?.author?.did === profileDid) {
- this.dataStore.$posts.set(post.uri, updateFunc(post));
- }
- }
- }
-
_updatePostsByThreadRoot(rootUri, updateFunc) {
for (const post of this.dataStore.$posts.values()) {
if (post?.viewer && getReplyRootFromPost(post).uri === rootUri) {
diff --git a/src/js/dataLayer/patchStore.js b/src/js/dataLayer/patchStore.js
index f00824e0..55e531be 100644
--- a/src/js/dataLayer/patchStore.js
+++ b/src/js/dataLayer/patchStore.js
@@ -216,6 +216,7 @@ export class PatchStore extends ReactiveStore {
viewer: {
...profile.viewer,
muted: true,
+ mutedOnlyReposts: false,
},
};
case "unmuteProfile":
@@ -224,6 +225,23 @@ export class PatchStore extends ReactiveStore {
viewer: {
...profile.viewer,
muted: false,
+ mutedOnlyReposts: false,
+ },
+ };
+ case "muteProfileReposts":
+ return {
+ ...profile,
+ viewer: {
+ ...profile.viewer,
+ mutedOnlyReposts: true,
+ },
+ };
+ case "unmuteProfileReposts":
+ return {
+ ...profile,
+ viewer: {
+ ...profile.viewer,
+ mutedOnlyReposts: false,
},
};
case "blockProfile":
diff --git a/src/js/profileInteractionHandler.js b/src/js/profileInteractionHandler.js
index 8280d4cb..4daa459d 100644
--- a/src/js/profileInteractionHandler.js
+++ b/src/js/profileInteractionHandler.js
@@ -60,6 +60,26 @@ export class ProfileInteractionHandler {
}
}
+ async handleMuteReposts(profile, doMute) {
+ if (doMute) {
+ try {
+ await this.dataLayer.mutations.muteProfileReposts(profile);
+ showToast("Reposts will be hidden in feeds");
+ } catch (error) {
+ console.error(error);
+ showToast("Failed to hide reposts", { style: "error" });
+ }
+ } else {
+ try {
+ await this.dataLayer.mutations.unmuteProfileReposts(profile);
+ showToast("Reposts will be shown in feeds");
+ } catch (error) {
+ console.error(error);
+ showToast("Failed to show reposts", { style: "error" });
+ }
+ }
+ }
+
async handleBlock(profile, doBlock) {
if (doBlock) {
const confirmed = await confirmModal(
diff --git a/src/js/templates/profileCard.template.js b/src/js/templates/profileCard.template.js
index 5a0a6e46..28516b3a 100644
--- a/src/js/templates/profileCard.template.js
+++ b/src/js/templates/profileCard.template.js
@@ -129,6 +129,7 @@ function profileContextMenuTemplate({
pluginItems,
onClickFollow,
onClickMute,
+ onClickMuteReposts,
onClickBlock,
onClickAddToLists,
onClickReport,
@@ -137,6 +138,11 @@ function profileContextMenuTemplate({
liveStatus,
}) {
const isFollowing = profile.viewer?.following;
+ const mutedOnlyReposts = !!profile.viewer?.mutedOnlyReposts;
+ const canMuteReposts =
+ !profile.viewer?.blocking &&
+ !profile.viewer?.mutedByList &&
+ !profile.viewer?.muted;
const pluginGroups = [...groupBy(pluginItems, "pluginId").values()];
return html`
@@ -201,6 +207,20 @@ function profileContextMenuTemplate({
`
: null}
+ ${canMuteReposts
+ ? html` {
+ onClickMuteReposts(profile, !mutedOnlyReposts);
+ }}
+ >
+ ${mutedOnlyReposts
+ ? "Show reposts in feeds"
+ : "Hide reposts in feeds"}
+ `
+ : null}
`
: ""}
+ ${profile.viewer?.mutedOnlyReposts
+ ? html``
+ : ""}
@${profile.handle}
diff --git a/src/js/views/profile.view.js b/src/js/views/profile.view.js
index d35eb486..4f693f00 100644
--- a/src/js/views/profile.view.js
+++ b/src/js/views/profile.view.js
@@ -343,6 +343,8 @@ export default async function profileView({
profileInteractionHandler.handleFollow(profile, doFollow),
onClickMute: (profile, doMute) =>
profileInteractionHandler.handleMute(profile, doMute),
+ onClickMuteReposts: (profile, doMute) =>
+ profileInteractionHandler.handleMuteReposts(profile, doMute),
onClickBlock: async (profile, doBlock) => {
await profileInteractionHandler.handleBlock(profile, doBlock);
if (!doBlock) {
diff --git a/tests/e2e/mockServer.js b/tests/e2e/mockServer.js
index 25f499e2..7a1b73d7 100644
--- a/tests/e2e/mockServer.js
+++ b/tests/e2e/mockServer.js
@@ -1830,7 +1830,11 @@ export class MockServer {
const actor = body?.actor;
const profile = this.profiles.get(actor);
if (profile) {
- profile.viewer = { ...profile.viewer, muted: true };
+ profile.viewer = {
+ ...profile.viewer,
+ muted: !body.onlyReposts,
+ mutedOnlyReposts: !!body.onlyReposts,
+ };
}
return route.fulfill({
status: 200,
@@ -1844,7 +1848,11 @@ export class MockServer {
const actor = body?.actor;
const profile = this.profiles.get(actor);
if (profile) {
- profile.viewer = { ...profile.viewer, muted: false };
+ profile.viewer = {
+ ...profile.viewer,
+ muted: false,
+ mutedOnlyReposts: false,
+ };
}
return route.fulfill({
status: 200,
diff --git a/tests/e2e/specs/views/profile.view.test.js b/tests/e2e/specs/views/profile.view.test.js
index daca683c..e44d26f8 100644
--- a/tests/e2e/specs/views/profile.view.test.js
+++ b/tests/e2e/specs/views/profile.view.test.js
@@ -841,12 +841,15 @@ test.describe("Profile view", () => {
await view.locator(".ellipsis-button").click();
const menu = page.locator(".profile-context-menu");
- await expect(menu.locator("context-menu-item")).toHaveCount(7, {
+ await expect(menu.locator("context-menu-item")).toHaveCount(8, {
timeout: 5000,
});
await expect(
menu.locator('[data-testid="menu-action-profile-open-in-bsky"]'),
).toBeVisible();
+ await expect(
+ menu.locator('[data-testid="menu-action-profile-mute-reposts"]'),
+ ).toBeVisible();
await expect(
menu.locator('[data-testid="menu-action-profile-copy-link"]'),
).toBeVisible();
@@ -905,6 +908,77 @@ test.describe("Profile view", () => {
).not.toBeVisible();
});
+ test("should hide and show reposts from the context menu", async ({
+ page,
+ }) => {
+ const mockServer = new MockServer();
+ mockServer.addProfile(otherUser);
+ await mockServer.setup(page);
+ await login(page);
+ await page.goto(`/profile/${otherUser.did}`);
+
+ const view = page.locator("#profile-view");
+ await expect(view.locator('[data-testid="profile-name"]')).toContainText(
+ "Other User",
+ { timeout: 10000 },
+ );
+ await expect(
+ view.locator('[data-testid="reposts-hidden-badge"]'),
+ ).not.toBeVisible();
+
+ const menu = page.locator(".profile-context-menu");
+ await view.locator(".ellipsis-button").click();
+ await menu
+ .locator(
+ '[data-testid="menu-action-profile-mute-reposts"][data-teststate="unmuted"]',
+ )
+ .click();
+
+ await expect(
+ view.locator('[data-testid="reposts-hidden-badge"]'),
+ ).toBeVisible({ timeout: 5000 });
+
+ await view.locator(".ellipsis-button").click();
+ await menu
+ .locator(
+ '[data-testid="menu-action-profile-mute-reposts"][data-teststate="muted"]',
+ )
+ .click();
+
+ await expect(
+ view.locator('[data-testid="reposts-hidden-badge"]'),
+ ).not.toBeVisible({ timeout: 5000 });
+ });
+
+ test("should not offer hiding reposts for fully muted profiles", async ({
+ page,
+ }) => {
+ const mutedUser = {
+ ...otherUser,
+ viewer: { ...otherUser.viewer, muted: true },
+ };
+ const mockServer = new MockServer();
+ mockServer.addProfile(mutedUser);
+ await mockServer.setup(page);
+ await login(page);
+ await page.goto(`/profile/${mutedUser.did}`);
+
+ const view = page.locator("#profile-view");
+ await expect(view.locator('[data-testid="profile-name"]')).toContainText(
+ "Other User",
+ { timeout: 10000 },
+ );
+ await view.locator(".ellipsis-button").click();
+
+ const menu = page.locator(".profile-context-menu");
+ await expect(
+ menu.locator('[data-testid="menu-action-profile-mute"]'),
+ ).toBeVisible({ timeout: 5000 });
+ await expect(
+ menu.locator('[data-testid="menu-action-profile-mute-reposts"]'),
+ ).toHaveCount(0);
+ });
+
test("should show 'Unblock Account' in context menu for blocked profiles", async ({
page,
}) => {
diff --git a/tests/unit/specs/api.test.js b/tests/unit/specs/api.test.js
index 1707f11d..b96b6145 100644
--- a/tests/unit/specs/api.test.js
+++ b/tests/unit/specs/api.test.js
@@ -1543,6 +1543,23 @@ describe("muteActor", () => {
});
});
+describe("muteActorReposts", () => {
+ it("should mute only the actor's reposts", async () => {
+ const session = createMockSession({});
+ const api = new Api(session);
+
+ await api.muteActorReposts("did:plc:target");
+
+ const { url, options } = session.getLastFetchOptions();
+ assert(url.includes("app.bsky.graph.muteActor"));
+ assert.deepEqual(options.method, "POST");
+ assert.deepEqual(JSON.parse(options.body), {
+ actor: "did:plc:target",
+ onlyReposts: true,
+ });
+ });
+});
+
describe("unmuteActor", () => {
it("should unmute actor", async () => {
const session = createMockSession({});
diff --git a/tests/unit/specs/dataLayer/derived.test.js b/tests/unit/specs/dataLayer/derived.test.js
index 31890731..54531d32 100644
--- a/tests/unit/specs/dataLayer/derived.test.js
+++ b/tests/unit/specs/dataLayer/derived.test.js
@@ -2817,6 +2817,18 @@ describe("$isFollowPending / $isBlockPending / $isMutePending", () => {
assert.deepEqual(derived.$isMutePending.get(did), true);
});
+ it("$isMutePending covers reposts-only mute patches", () => {
+ const dataStore = new DataStore(createSessionState(null));
+ const { derived, patchStore } = makeDerived(dataStore);
+ const muteId = patchStore.addProfilePatch(did, {
+ type: "muteProfileReposts",
+ });
+ assert.deepEqual(derived.$isMutePending.get(did), true);
+ patchStore.removeProfilePatch(did, muteId);
+ patchStore.addProfilePatch(did, { type: "unmuteProfileReposts" });
+ assert.deepEqual(derived.$isMutePending.get(did), true);
+ });
+
it("pending signals are keyed per profile", () => {
const dataStore = new DataStore(createSessionState(null));
const { derived, patchStore } = makeDerived(dataStore);
diff --git a/tests/unit/specs/dataLayer/mutations.test.js b/tests/unit/specs/dataLayer/mutations.test.js
index 99827f84..c83381c1 100644
--- a/tests/unit/specs/dataLayer/mutations.test.js
+++ b/tests/unit/specs/dataLayer/mutations.test.js
@@ -12,6 +12,7 @@ import { HiddenFeedItemsStore } from "/js/dataLayer/hiddenFeedItemsStore.js";
import { CDN_URL } from "/js/config.js";
import { ApiError } from "/js/api.js";
import { trackDisposable } from "../../testHelpers.js";
+import { isBlockedPost } from "/js/dataHelpers.js";
import { createPost } from "../../../shared/factories.js";
function escapeRegExp(value) {
@@ -1883,6 +1884,85 @@ describe("unmuteProfile", () => {
});
});
+describe("muteProfileReposts / unmuteProfileReposts", () => {
+ const did = "did:plc:target";
+
+ function setup(mockApi = {}) {
+ const dataStore = new DataStore(createSessionState(null));
+ const patchStore = new PatchStore();
+ const mockPreferencesProvider = {
+ requirePreferences: () => Preferences.createLoggedOutPreferences(),
+ };
+ const mutations = makeMutations(
+ {
+ muteActorReposts: async () => ({}),
+ unmuteActor: async () => ({}),
+ ...mockApi,
+ },
+ dataStore,
+ patchStore,
+ mockPreferencesProvider,
+ );
+ return { mutations, dataStore, patchStore };
+ }
+
+ it("muteProfileReposts calls muteActorReposts and sets viewer.mutedOnlyReposts", async () => {
+ const muteActorReposts = mock.fn(async () => ({}));
+ const { mutations, dataStore } = setup({ muteActorReposts });
+
+ await mutations.muteProfileReposts({ did, viewer: {} });
+
+ assert.deepEqual(muteActorReposts.mock.calls[0].arguments, [did]);
+ assert.deepEqual(
+ dataStore.$profiles.get(did).viewer.mutedOnlyReposts,
+ true,
+ );
+ });
+
+ it("unmuteProfileReposts calls unmuteActor and clears viewer.mutedOnlyReposts", async () => {
+ const unmuteActor = mock.fn(async () => ({}));
+ const { mutations, dataStore } = setup({ unmuteActor });
+
+ await mutations.unmuteProfileReposts({
+ did,
+ viewer: { mutedOnlyReposts: true },
+ });
+
+ assert.deepEqual(unmuteActor.mock.calls[0].arguments, [did]);
+ assert.deepEqual(
+ dataStore.$profiles.get(did).viewer.mutedOnlyReposts,
+ false,
+ );
+ });
+
+ it("muteProfile replaces a reposts-only mute", async () => {
+ const { mutations, dataStore } = setup({ muteActor: async () => ({}) });
+
+ await mutations.muteProfile({ did, viewer: { mutedOnlyReposts: true } });
+
+ const viewer = dataStore.$profiles.get(did).viewer;
+ assert.deepEqual(viewer.muted, true);
+ assert.deepEqual(viewer.mutedOnlyReposts, false);
+ });
+
+ it("rethrows and clears the pending patch on failure", async (t) => {
+ t.mock.method(console, "error", () => {});
+ const { mutations, patchStore } = setup({
+ muteActorReposts: async () => {
+ throw new Error("boom");
+ },
+ });
+ await assert.rejects(
+ mutations.muteProfileReposts({ did, viewer: {} }),
+ /boom/,
+ );
+ assert.deepEqual(
+ patchStore.hasPendingProfilePatch(did, ["muteProfileReposts"]),
+ false,
+ );
+ });
+});
+
describe("blockProfile", () => {
const profile = {
did: "did:plc:target",
@@ -1897,13 +1977,14 @@ describe("blockProfile", () => {
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
};
+ const derived = makeDerived(dataStore, patchStore, mockPreferencesProvider);
const mutations = makeMutations(
{ blockActor: async () => ({ uri: blockUri }), ...mockApi },
dataStore,
patchStore,
mockPreferencesProvider,
);
- return { mutations, dataStore };
+ return { mutations, dataStore, derived };
}
it("should set viewer.blocking on the profile", async () => {
@@ -1950,29 +2031,22 @@ describe("blockProfile", () => {
assert.deepEqual(dataStore.$blockedProfiles.get(), null);
});
- it("should update author viewer.blocking on cached posts by that author", async () => {
- const { mutations, dataStore } = setup();
- const post = {
+ it("should render cached posts by that author as blocked", async () => {
+ const { mutations, dataStore, derived } = setup();
+ const post = createPost({
uri: "at://did:plc:target/app.bsky.feed.post/1",
- author: { did: profile.did, viewer: {} },
- };
- const otherPost = {
+ authorHandle: profile.handle,
+ });
+ const otherPost = createPost({
uri: "at://did:plc:someone/app.bsky.feed.post/1",
- author: { did: "did:plc:someone", viewer: {} },
- };
- dataStore.$posts.set(post.uri, post);
- dataStore.$posts.set(otherPost.uri, otherPost);
+ authorHandle: "someone.test",
+ });
+ dataStore.setPosts([post, otherPost]);
await mutations.blockProfile(profile);
- assert.deepEqual(
- dataStore.$posts.get(post.uri).author.viewer.blocking,
- blockUri,
- );
- assert.deepEqual(
- dataStore.$posts.get(otherPost.uri).author.viewer.blocking,
- undefined,
- );
+ assert(isBlockedPost(derived.$hydratedPosts.get(post.uri)));
+ assert(!isBlockedPost(derived.$hydratedPosts.get(otherPost.uri)));
});
});
@@ -1989,13 +2063,14 @@ describe("unblockProfile", () => {
const mockPreferencesProvider = {
requirePreferences: () => Preferences.createLoggedOutPreferences(),
};
+ const derived = makeDerived(dataStore, patchStore, mockPreferencesProvider);
const mutations = makeMutations(
{ unblockActor: async () => ({}), ...mockApi },
dataStore,
patchStore,
mockPreferencesProvider,
);
- return { mutations, dataStore };
+ return { mutations, dataStore, derived };
}
it("should clear viewer.blocking on the profile", async () => {
@@ -2039,20 +2114,19 @@ describe("unblockProfile", () => {
assert.deepEqual(dataStore.$blockedProfiles.get().blocks.length, 1);
});
- it("should clear author viewer.blocking on cached posts by that author", async () => {
- const { mutations, dataStore } = setup();
- const post = {
+ it("should stop rendering cached posts by that author as blocked", async () => {
+ const { mutations, dataStore, derived } = setup();
+ const post = createPost({
uri: "at://did:plc:target/app.bsky.feed.post/1",
- author: { did: profile.did, viewer: { blocking: "at://old" } },
- };
- dataStore.$posts.set(post.uri, post);
+ authorHandle: profile.handle,
+ authorViewer: profile.viewer,
+ });
+ dataStore.setPosts([post]);
+ assert(isBlockedPost(derived.$hydratedPosts.get(post.uri)));
await mutations.unblockProfile(profile);
- assert.deepEqual(
- dataStore.$posts.get(post.uri).author.viewer.blocking,
- null,
- );
+ assert(!isBlockedPost(derived.$hydratedPosts.get(post.uri)));
});
});
diff --git a/tests/unit/specs/dataLayer/patchStore.test.js b/tests/unit/specs/dataLayer/patchStore.test.js
index b6f37eb4..6d82fdeb 100644
--- a/tests/unit/specs/dataLayer/patchStore.test.js
+++ b/tests/unit/specs/dataLayer/patchStore.test.js
@@ -1094,6 +1094,33 @@ describe("Profile Patches - Mute/Block/NotificationSubscription", () => {
assert.deepEqual(result.viewer.muted, false);
});
+ it("should apply muteProfileReposts / unmuteProfileReposts patches", () => {
+ const patchStore = new PatchStore();
+ patchStore.addProfilePatch(did, { type: "muteProfileReposts" });
+ let result = applyProfilePatchesForDid(patchStore, baseProfile);
+ assert.deepEqual(result.viewer.mutedOnlyReposts, true);
+ assert.deepEqual(result.viewer.muted, false);
+
+ const patchStore2 = new PatchStore();
+ patchStore2.addProfilePatch(did, { type: "unmuteProfileReposts" });
+ result = applyProfilePatchesForDid(patchStore2, {
+ ...baseProfile,
+ viewer: { ...baseProfile.viewer, mutedOnlyReposts: true },
+ });
+ assert.deepEqual(result.viewer.mutedOnlyReposts, false);
+ });
+
+ it("should clear mutedOnlyReposts when applying a full muteProfile patch", () => {
+ const patchStore = new PatchStore();
+ patchStore.addProfilePatch(did, { type: "muteProfile" });
+ const result = applyProfilePatchesForDid(patchStore, {
+ ...baseProfile,
+ viewer: { ...baseProfile.viewer, mutedOnlyReposts: true },
+ });
+ assert.deepEqual(result.viewer.muted, true);
+ assert.deepEqual(result.viewer.mutedOnlyReposts, false);
+ });
+
it("should apply blockProfile / unblockProfile patches", () => {
const patchStore = new PatchStore();
patchStore.addProfilePatch(did, { type: "blockProfile" });
diff --git a/tests/unit/specs/templates/profileCard.template.test.js b/tests/unit/specs/templates/profileCard.template.test.js
index aeb33a3d..8c6ec1fd 100644
--- a/tests/unit/specs/templates/profileCard.template.test.js
+++ b/tests/unit/specs/templates/profileCard.template.test.js
@@ -210,6 +210,28 @@ describe("profileCardTemplate", () => {
);
});
+ it("should render reposts hidden badge only for reposts-only mutes", () => {
+ const container = document.createElement("div");
+ render(
+ profileCardTemplate({
+ profile: { ...mockProfile, viewer: { mutedOnlyReposts: true } },
+ }),
+ container,
+ );
+ assert(
+ container.querySelector("[data-testid='reposts-hidden-badge']") !== null,
+ );
+
+ render(
+ profileCardTemplate({ profile: { ...mockProfile, viewer: {} } }),
+ container,
+ );
+ assert.deepEqual(
+ container.querySelector("[data-testid='reposts-hidden-badge']"),
+ null,
+ );
+ });
+
it("should call onClickFollow when follow button clicked", () => {
let followCallArgs = null;
const profile = {