diff --git a/package.json b/package.json
index 8f442d80..40fe7465 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "impro",
- "version": "0.18.63",
+ "version": "0.18.64",
"type": "module",
"scripts": {
"start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve",
diff --git a/src/css/style.css b/src/css/style.css
index 949f57b7..6e415d35 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -5300,17 +5300,38 @@ emoji-picker-dialog emoji-picker,
gap: 8px;
padding: 12px 16px;
border-bottom: var(--hair) solid var(--post-border-color);
+}
+
+container-link.profile-list-item,
+button.profile-list-item-button {
cursor: pointer;
}
+button.profile-list-item-button {
+ display: flex;
+ background: none;
+ border: none;
+ color: inherit;
+ font: inherit;
+ text-align: left;
+ width: 100%;
+}
+
+button.profile-list-item-button.is-disabled {
+ cursor: not-allowed;
+ opacity: 0.55;
+}
+
.profile-list-item-row {
display: flex;
align-items: flex-start;
gap: 12px;
+ width: 100%;
}
@media (hover: hover) {
- .profile-list-item:hover {
+ container-link.profile-list-item:hover,
+ button.profile-list-item-button:not(:disabled):hover {
background: var(--post-hover-color);
}
}
@@ -9224,6 +9245,13 @@ toggle-switch {
padding: 20px 0;
}
+.new-chat-not-messageable-hint {
+ align-self: center;
+ color: var(--text-color-muted);
+ font-size: 0.85rem;
+ flex-shrink: 0;
+}
+
.new-chat-section-header {
color: var(--text-color-secondary);
font-size: 0.85rem;
@@ -9293,6 +9321,29 @@ toggle-switch {
border-radius: 4px;
}
+.manage-list-members-toggle {
+ align-self: center;
+ flex-shrink: 0;
+ min-width: 80px;
+}
+
+.manage-list-members-toggle .loading-spinner {
+ width: 14px;
+ height: 14px;
+}
+
+.manage-list-members-message {
+ color: var(--text-color-secondary);
+ text-align: center;
+ padding: 20px 0;
+}
+
+.manage-list-members-section-header {
+ color: var(--text-color-secondary);
+ font-size: 0.85rem;
+ padding: 4px 16px 8px;
+}
+
tab-bar[full-width] {
width: 100%;
}
diff --git a/src/js/components/manage-list-members-dialog.js b/src/js/components/manage-list-members-dialog.js
new file mode 100644
index 00000000..2e926eea
--- /dev/null
+++ b/src/js/components/manage-list-members-dialog.js
@@ -0,0 +1,310 @@
+import { html, render } from "/js/lib/lit-html.js";
+import { Component } from "/js/components/component.js";
+import { scrollLocks } from "/js/scrollLocks.js";
+import {
+ closeWithAnimation,
+ enableDragToDismiss,
+ resetScrollOnBlur,
+} from "/js/dialogHelpers.js";
+import { Signal, SignalSet, ReactiveStore, effect } from "/js/signals.js";
+import { searchIconTemplate } from "/js/templates/icons/searchIcon.template.js";
+import { closeIconTemplate } from "/js/templates/icons/closeIcon.template.js";
+import { profileFeedTemplate } from "/js/templates/profileFeed.template.js";
+import { classnames } from "/js/utils.js";
+
+const MAX_MEMBER_PAGES = 6;
+
+function toggleButtonTemplate({ profile, isMember, isPending, onToggle }) {
+ return html``;
+}
+
+function profileListTemplate({ profiles, emptyMessage, rightItemTemplate }) {
+ return profileFeedTemplate({
+ profiles,
+ hasMore: false,
+ clickAction: "none",
+ compact: true,
+ rightItemTemplate,
+ emptyMessage,
+ });
+}
+
+class ManageListMembersDialog extends Component {
+ connectedCallback() {
+ if (this.initialized) {
+ return;
+ }
+ this.dataLayer = this.dataLayer ?? null;
+ this.list = this.list ?? null;
+ this.setAttribute("data-dialog-wrapper", "");
+ this.scrollLock = null;
+ this.state = new ReactiveStore("manage-list-members-dialog");
+ this.state.$query = new Signal.State("");
+ this.state.$pendingDids = new SignalSet();
+ this.state.$membersLoaded = new Signal.State(false);
+ this.innerHTML = "";
+ this._disposeEffect = effect(() => {
+ this.render();
+ });
+ this._loadSuggestions();
+ this._loadAllMembers();
+ this.initialized = true;
+ }
+
+ disconnectedCallback() {
+ this._disposeEffect?.();
+ this._disposeEffect = null;
+ }
+
+ async _loadSuggestions() {
+ try {
+ const currentUser = await this.dataLayer.declarative.ensureCurrentUser();
+ await this.dataLayer.declarative.ensureProfileFollows(currentUser.did);
+ } catch (error) {
+ console.warn("Failed to load suggested profiles", error);
+ }
+ }
+
+ async _loadAllMembers() {
+ const listUri = this.list.uri;
+ try {
+ await this.dataLayer.requests.loadListMembers(listUri, { reload: true });
+ for (let i = 1; i < MAX_MEMBER_PAGES; i++) {
+ const data = this.dataLayer.dataStore.$listMembers.get(listUri);
+ if (!data?.cursor) break;
+ await this.dataLayer.requests.loadListMembers(listUri);
+ }
+ } catch (error) {
+ console.warn("Failed to load list members", error);
+ } finally {
+ this.state.$membersLoaded.set(true);
+ }
+ }
+
+ _onSearchInput(value) {
+ this.state.$query.set(value);
+ const query = value.trim();
+ if (!query) {
+ this.dataLayer.requests.loadChatRecipientSearch("");
+ } else {
+ this.dataLayer.requests.loadChatRecipientSearch(query, { limit: 12 });
+ }
+ const results = this.querySelector(".new-chat-results");
+ if (results) results.scrollTop = 0;
+ }
+
+ _onClearSearch() {
+ this._onSearchInput("");
+ }
+
+ async _onToggle(profile) {
+ if (this.state.$pendingDids.has(profile.did)) return;
+ const memberDidToUri = this._memberDidToUri();
+ const membershipUri = memberDidToUri.get(profile.did);
+ this.state.$pendingDids.add(profile.did);
+ try {
+ if (membershipUri) {
+ await this.dataLayer.mutations.removeProfileFromList(
+ profile,
+ this.list,
+ membershipUri,
+ );
+ } else {
+ await this.dataLayer.mutations.addProfileToList(profile, this.list);
+ }
+ } catch (error) {
+ console.error(error);
+ } finally {
+ this.state.$pendingDids.delete(profile.did);
+ }
+ }
+
+ _memberDidToUri() {
+ const data = this.dataLayer.dataStore.$listMembers.get(this.list.uri);
+ const map = new Map();
+ if (!data) return map;
+ for (const item of data.items) {
+ map.set(item.subject.did, item.uri);
+ }
+ return map;
+ }
+
+ render() {
+ const query = this.state.$query.get().trim();
+ const currentUserDid = this.dataLayer.derived.$currentUser.get()?.did;
+ const searchResults =
+ this.dataLayer.derived.$chatRecipientSearchResults.get();
+ const searchStatus = this.dataLayer.requests.statusStore.$statuses.get(
+ "loadChatRecipientSearch",
+ );
+ const profileFollows = currentUserDid
+ ? this.dataLayer.derived.$profileFollows.get(currentUserDid)?.follows
+ : null;
+ const profileFollowsStatus = currentUserDid
+ ? this.dataLayer.requests.statusStore.$statuses.get(
+ `loadProfileFollows-${currentUserDid}`,
+ )
+ : null;
+ const memberDidToUri = this._memberDidToUri();
+ const membersLoaded = this.state.$membersLoaded.get();
+ const pendingDids = this.state.$pendingDids;
+ const rightItemTemplate = (profile) => {
+ if (!membersLoaded) return "";
+ return toggleButtonTemplate({
+ profile,
+ isMember: memberDidToUri.has(profile.did),
+ isPending: pendingDids.has(profile.did),
+ onToggle: (p) => this._onToggle(p),
+ });
+ };
+ render(
+ html`
+
+ `,
+ this,
+ );
+ }
+
+ open() {
+ this.scrollLock ??= scrollLocks.acquire({ target: this });
+ const dialog = this.querySelector(".manage-list-members-dialog");
+ if (dialog?.open) return;
+ dialog.showModal();
+ this.querySelector(".new-chat-search-input")?.focus({
+ preventScroll: true,
+ });
+ enableDragToDismiss(dialog, {
+ onClose: () => this.close(),
+ scrollContainer: this.querySelector(".new-chat-results"),
+ ignoreTouchTarget: (element) => element.closest("button, input") !== null,
+ });
+ resetScrollOnBlur(dialog, this.querySelector(".new-chat-results"));
+ }
+
+ close() {
+ return closeWithAnimation(
+ this.querySelector(".manage-list-members-dialog"),
+ );
+ }
+}
+
+ManageListMembersDialog.register();
diff --git a/src/js/components/new-chat-dialog.js b/src/js/components/new-chat-dialog.js
index 3c99419b..70caae36 100644
--- a/src/js/components/new-chat-dialog.js
+++ b/src/js/components/new-chat-dialog.js
@@ -7,10 +7,9 @@ import {
resetScrollOnBlur,
} from "/js/dialogHelpers.js";
import { Signal, ReactiveStore, effect } from "/js/signals.js";
-import { getDisplayName, MISSING_HANDLE } from "/js/dataHelpers.js";
-import { avatarTemplate } from "/js/templates/avatar.template.js";
import { searchIconTemplate } from "/js/templates/icons/searchIcon.template.js";
import { closeIconTemplate } from "/js/templates/icons/closeIcon.template.js";
+import { profileFeedTemplate } from "/js/templates/profileFeed.template.js";
import { showToast } from "/js/toasts.js";
const CREATE_CHAT_ERROR_TOASTS = {
@@ -61,84 +60,29 @@ function partitionRows(profiles, currentUserDid) {
];
}
-function skeletonTemplate() {
- return html`${Array.from({ length: 6 }).map(
- () => html`
-
- `,
- )}`;
+function notMessageableRightItem(profile) {
+ if (canBeMessaged(profile)) return null;
+ return html`
+ Can't be messaged
+
`;
}
-function resultRowTemplate({ profile, onSelect }) {
- const isMessageable = canBeMessaged(profile);
- const hasHandle = profile.handle && profile.handle !== MISSING_HANDLE;
- return html`
-
- `;
-}
-
-function searchResultsTemplate({ status, rows, onSelect }) {
- if (status.error) {
- return html`
- We're having network issues, try again
-
`;
- }
- if (!rows || (rows.length === 0 && status.loading)) {
- return skeletonTemplate();
- }
- if (rows.length === 0) {
- return html`
- No results
-
`;
- }
- return rows.map((profile) => resultRowTemplate({ profile, onSelect }));
-}
-
-function suggestionsTemplate({ status, rows, onSelect }) {
- if (!rows && !status?.error) {
- return skeletonTemplate();
- }
- if (!rows || rows.length === 0) {
- return html`
- Search for someone to message
-
`;
- }
- return html`
-
- ${rows.map((profile) => resultRowTemplate({ profile, onSelect }))}
- `;
+function profileListTemplate({ profiles, onSelect, emptyMessage = null }) {
+ return profileFeedTemplate({
+ profiles,
+ hasMore: false,
+ skeletonCount: 6,
+ compact: true,
+ clickAction: onSelect,
+ rightItemTemplate: notMessageableRightItem,
+ disabledProfiles: (profiles ?? [])
+ .filter((profile) => !canBeMessaged(profile))
+ .map((profile) => profile.did),
+ emptyMessage,
+ });
}
class NewChatDialog extends Component {
@@ -181,6 +125,8 @@ class NewChatDialog extends Component {
} else {
this.dataLayer.requests.loadChatRecipientSearch(query, { limit: 12 });
}
+ const results = this.querySelector(".new-chat-results");
+ if (results) results.scrollTop = 0;
}
_onClearSearch() {
@@ -202,16 +148,15 @@ class NewChatDialog extends Component {
render() {
const query = this.state.$query.get().trim();
- const onSelect = (profile) => this._onSelect(profile);
const currentUserDid = this.dataLayer.derived.$currentUser.get()?.did;
const results = this.dataLayer.derived.$chatRecipientSearchResults.get();
const searchStatus = this.dataLayer.requests.statusStore.$statuses.get(
"loadChatRecipientSearch",
);
- const follows = currentUserDid
+ const profileFollows = currentUserDid
? this.dataLayer.derived.$profileFollows.get(currentUserDid)?.follows
: null;
- const followsStatus = currentUserDid
+ const profileFollowsStatus = currentUserDid
? this.dataLayer.requests.statusStore.$statuses.get(
`loadProfileFollows-${currentUserDid}`,
)
@@ -278,24 +223,52 @@ class NewChatDialog extends Component {
: ""}
- ${query
- ? searchResultsTemplate({
- status: searchStatus,
- rows: results
+ ${(() => {
+ if (query) {
+ if (searchStatus?.error) {
+ return html`
+ We're having network issues, try again
+
`;
+ }
+ return profileListTemplate({
+ profiles: results
? partitionRows(results, currentUserDid)
: null,
- onSelect,
- })
- : suggestionsTemplate({
- status: followsStatus,
- rows: follows
- ? partitionRows(
- follows.filter((profile) => canBeMessaged(profile)),
- currentUserDid,
- )
- : null,
- onSelect,
+ onSelect: (profile) => this._onSelect(profile),
+ emptyMessage: "No results",
+ });
+ }
+ let suggestedProfiles = null;
+ if (profileFollowsStatus?.error) {
+ suggestedProfiles = [];
+ } else if (profileFollows) {
+ suggestedProfiles = partitionRows(
+ profileFollows.filter((profile) => canBeMessaged(profile)),
+ currentUserDid,
+ );
+ }
+ if (!suggestedProfiles?.length) {
+ return profileListTemplate({
+ profiles: suggestedProfiles,
+ emptyMessage: "Search for someone to message",
+ });
+ }
+ return html`
+
+ ${profileListTemplate({
+ profiles: suggestedProfiles,
+ onSelect: (profile) => this._onSelect(profile),
})}
+ `;
+ })()}
diff --git a/src/js/components/plugin-profiles-list.js b/src/js/components/plugin-profiles-list.js
index 9a05428a..acfdd47c 100644
--- a/src/js/components/plugin-profiles-list.js
+++ b/src/js/components/plugin-profiles-list.js
@@ -41,7 +41,7 @@ class PluginProfilesList extends Component {
hasMore: false,
skeletonCount: dids.length,
emptyMessage,
- showFollowButton: false,
+ rightItemTemplate: null,
}),
this,
);
diff --git a/src/js/templates/profileFeed.template.js b/src/js/templates/profileFeed.template.js
index 0cd09bc6..28f5307c 100644
--- a/src/js/templates/profileFeed.template.js
+++ b/src/js/templates/profileFeed.template.js
@@ -9,102 +9,164 @@ import { getDisplayName } from "/js/dataHelpers.js";
import { classnames } from "/js/utils.js";
import "/js/components/container-link.js";
+// clickAction: "link" | "none" | callback
+function itemWrapperTemplate({ actor, clickAction, isDisabled, children }) {
+ if (typeof clickAction === "function") {
+ return html``;
+ }
+ if (clickAction === "none") {
+ return html`${children}
`;
+ }
+ return html`${children}`;
+}
+
+function nameTemplate({ actor, doLink }) {
+ const content = html`
+ ${getDisplayName(actor)}${verificationBadgeTemplate({
+ profile: actor,
+ })}${automatedAccountBadgeTemplate({ profile: actor })}
+ `;
+ if (doLink) {
+ return html`${content}`;
+ }
+ return html`${content}`;
+}
+
+function followButtonRightItem({
+ actor,
+ isAuthenticated,
+ currentUserDid,
+ profileInteractionHandler,
+}) {
+ const isCurrentUser = currentUserDid && actor.did === currentUserDid;
+ const isBlocking = !!actor.viewer?.blocking;
+ const isBlockedBy = !!actor.viewer?.blockedBy;
+ if (isCurrentUser || !isAuthenticated || isBlocking || isBlockedBy) {
+ return null;
+ }
+ const isFollowing = !!actor.viewer?.following;
+ const isFollowedBy = !!actor.viewer?.followedBy;
+ return html``;
+}
+
export function profileListItemTemplate({
actor,
isAuthenticated = false,
currentUserDid = null,
profileInteractionHandler = null,
- showFollowButton = true,
- badgeTemplate = null,
+ rightItemTemplate,
+ clickAction = "link",
+ compact = false,
+ isDisabled = false,
}) {
- const displayName = getDisplayName(actor);
- const isCurrentUser = currentUserDid && actor.did === currentUserDid;
- const isFollowing = !!actor.viewer?.following;
const isFollowedBy = !!actor.viewer?.followedBy;
const isBlocking = !!actor.viewer?.blocking;
const isBlockedBy = !!actor.viewer?.blockedBy;
const showsFollowsYou = isFollowedBy && !isBlocking && !isBlockedBy;
- const showsFollowControl =
- showFollowButton &&
- !isCurrentUser &&
- isAuthenticated &&
- !isBlocking &&
- !isBlockedBy;
const description = actor.description?.trim();
- return html`
-
- ${avatarTemplate({ author: actor })}
-
-
-
- ${displayName}${verificationBadgeTemplate({
- profile: actor,
- })}${automatedAccountBadgeTemplate({ profile: actor })}
-
-
+ // Render follow button by default
+ const rightItem =
+ rightItemTemplate === undefined
+ ? (followButtonRightItem({
+ actor,
+ isAuthenticated,
+ currentUserDid,
+ profileInteractionHandler,
+ }) ?? "")
+ : rightItemTemplate === null
+ ? ""
+ : (rightItemTemplate(actor) ?? "");
+ return itemWrapperTemplate({
+ actor,
+ clickAction,
+ isDisabled,
+ children: html`
+
+ ${avatarTemplate({
+ author: actor,
+ clickAction: clickAction === "link" ? "link" : "none",
+ })}
- @${actor.handle}
+ ${nameTemplate({ actor, doLink: clickAction === "link" })}
+
+ @${actor.handle}
+
+ ${rightItem}
- ${badgeTemplate ? (badgeTemplate(actor) ?? "") : ""}
- ${showsFollowControl
- ? html`
- ${showsFollowsYou
- ? html`
- Follows you
-
`
- : ""}
- ${description
- ? html`
- ${richTextTemplate({ text: description })}
-
`
- : ""}
- `;
+ `,
+ });
}
-export function profileListItemSkeletonTemplate() {
+export function profileListItemSkeletonTemplate({ compact = false } = {}) {
return html`
-
+ ${compact
+ ? ""
+ : html`
`}
`;
}
@@ -133,13 +197,15 @@ export function profileFeedTemplate({
isAuthenticated = false,
currentUserDid = null,
profileInteractionHandler = null,
- showFollowButton = true,
- badgeTemplate = null,
+ rightItemTemplate,
+ clickAction = "link",
+ compact = false,
+ disabledProfiles = null,
}) {
if (!profiles) {
return html`
${Array.from({ length: skeletonCount }).map(() =>
- profileListItemSkeletonTemplate(),
+ profileListItemSkeletonTemplate({ compact }),
)}
`;
}
@@ -148,6 +214,7 @@ export function profileFeedTemplate({
${emptyMessage ?? "No profiles to show."}
`;
}
+ const disabledSet = disabledProfiles ? new Set(disabledProfiles) : null;
return html` {
@@ -164,8 +231,10 @@ export function profileFeedTemplate({
isAuthenticated,
currentUserDid,
profileInteractionHandler,
- showFollowButton,
- badgeTemplate,
+ rightItemTemplate,
+ clickAction,
+ compact,
+ isDisabled: disabledSet ? disabledSet.has(profile.did) : false,
}),
)}
diff --git a/src/js/views/groupChatDetails.view.js b/src/js/views/groupChatDetails.view.js
index af781ca8..bdeced4d 100644
--- a/src/js/views/groupChatDetails.view.js
+++ b/src/js/views/groupChatDetails.view.js
@@ -195,9 +195,8 @@ class GroupChatDetailsView extends View {
hasMore,
onLoadMore: loadMoreMembers,
isAuthenticated,
- currentUserDid: currentUser?.did ?? null,
- showFollowButton: false,
- badgeTemplate: (actor) =>
+ compact: true,
+ rightItemTemplate: (actor) =>
memberTrailingTemplate({ member: actor, ownerDid }),
})}`;
})()}
diff --git a/src/js/views/listDetail.view.js b/src/js/views/listDetail.view.js
index f1640cec..fd321473 100644
--- a/src/js/views/listDetail.view.js
+++ b/src/js/views/listDetail.view.js
@@ -17,6 +17,7 @@ import "/js/components/infinite-scroll-container.js";
import "/js/components/context-menu.js";
import "/js/components/context-menu-item.js";
import "/js/components/edit-list-details-dialog.js";
+import "/js/components/manage-list-members-dialog.js";
class ListDetailView extends View {
async render({
@@ -176,6 +177,12 @@ class ListDetailView extends View {
currentUser?.did &&
listCreator.did === currentUser.did
? html` handleAddPeople(list)}
+ >
+ Add people to list
+
+ handleEditList(list)}
>
@@ -294,7 +301,7 @@ class ListDetailView extends View {
isAuthenticated,
currentUserDid: currentUser?.did ?? null,
profileInteractionHandler,
- showFollowButton: isCurateList,
+ ...(isCurateList ? {} : { rightItemTemplate: null }),
})}
`}
@@ -313,6 +320,17 @@ class ListDetailView extends View {
window.router.back({ fallbackRoute });
}
+ function handleAddPeople(list) {
+ const dialog = document.createElement("manage-list-members-dialog");
+ dialog.dataLayer = dataLayer;
+ dialog.list = list;
+ dialog.addEventListener("dialog-closed", () => {
+ dialog.remove();
+ });
+ root.querySelector("main").appendChild(dialog);
+ dialog.open();
+ }
+
async function handleEditList(list) {
const dialog = document.createElement("edit-list-details-dialog");
dialog.addEventListener("list-save", async (event) => {
diff --git a/src/js/views/settings/blockedAccounts.view.js b/src/js/views/settings/blockedAccounts.view.js
index 26cbf2f1..7da9d7e4 100644
--- a/src/js/views/settings/blockedAccounts.view.js
+++ b/src/js/views/settings/blockedAccounts.view.js
@@ -7,7 +7,12 @@ import { profileFeedTemplate } from "/js/templates/profileFeed.template.js";
import "/js/components/infinite-scroll-container.js";
class SettingsBlockedAccountsView extends View {
- async render({ root, router, layout, context: { dataLayer } }) {
+ async render({
+ root,
+ router,
+ layout,
+ context: { dataLayer, isAuthenticated },
+ }) {
await auth.requireAuth();
async function loadMore() {
@@ -61,7 +66,7 @@ class SettingsBlockedAccountsView extends View {
hasMore,
onLoadMore: loadMore,
emptyMessage: "You haven't blocked any accounts.",
- showFollowButton: false,
+ isAuthenticated,
});
})()}
diff --git a/src/js/views/settings/mutedAccounts.view.js b/src/js/views/settings/mutedAccounts.view.js
index 15b0f742..d6553358 100644
--- a/src/js/views/settings/mutedAccounts.view.js
+++ b/src/js/views/settings/mutedAccounts.view.js
@@ -7,7 +7,12 @@ import { profileFeedTemplate } from "/js/templates/profileFeed.template.js";
import "/js/components/infinite-scroll-container.js";
class SettingsMutedAccountsView extends View {
- async render({ root, router, layout, context: { dataLayer } }) {
+ async render({
+ root,
+ router,
+ layout,
+ context: { dataLayer, isAuthenticated },
+ }) {
await auth.requireAuth();
async function loadMore() {
@@ -57,7 +62,8 @@ class SettingsMutedAccountsView extends View {
hasMore,
onLoadMore: loadMore,
emptyMessage: "You have not muted any accounts yet.",
- showFollowButton: false,
+ isAuthenticated,
+ rightItemTemplate: null,
});
})()}
diff --git a/tests/e2e/specs/views/settings/blockedAccounts.view.test.js b/tests/e2e/specs/views/settings/blockedAccounts.view.test.js
index abacb3b2..11e94c6e 100644
--- a/tests/e2e/specs/views/settings/blockedAccounts.view.test.js
+++ b/tests/e2e/specs/views/settings/blockedAccounts.view.test.js
@@ -66,4 +66,23 @@ test.describe("Settings Blocked Accounts view", () => {
await expect(view).toContainText("Alice");
await expect(view).toContainText("Bob");
});
+
+ test("should not render follow buttons on blocked accounts", async ({
+ page,
+ }) => {
+ const mockServer = new MockServer();
+ mockServer.blockedProfiles = [alice, bob];
+ mockServer.addProfile(alice);
+ mockServer.addProfile(bob);
+ await mockServer.setup(page);
+
+ await login(page);
+ await page.goto("/settings/blocked-accounts");
+
+ const view = page.locator("#settings-blocked-accounts-view");
+ await expect(view.locator(".profile-list-item")).toHaveCount(2, {
+ timeout: 10000,
+ });
+ await expect(view.locator('[data-testid="follow-button"]')).toHaveCount(0);
+ });
});
diff --git a/tests/e2e/specs/views/settings/mutedAccounts.view.test.js b/tests/e2e/specs/views/settings/mutedAccounts.view.test.js
index 0fd21e65..5574f13a 100644
--- a/tests/e2e/specs/views/settings/mutedAccounts.view.test.js
+++ b/tests/e2e/specs/views/settings/mutedAccounts.view.test.js
@@ -66,4 +66,23 @@ test.describe("Settings Muted Accounts view", () => {
await expect(view).toContainText("Alice");
await expect(view).toContainText("Bob");
});
+
+ test("should not render follow buttons on muted accounts", async ({
+ page,
+ }) => {
+ const mockServer = new MockServer();
+ mockServer.mutedProfiles = [alice, bob];
+ mockServer.addProfile(alice);
+ mockServer.addProfile(bob);
+ await mockServer.setup(page);
+
+ await login(page);
+ await page.goto("/settings/muted-accounts");
+
+ const view = page.locator("#settings-muted-accounts-view");
+ await expect(view.locator(".profile-list-item")).toHaveCount(2, {
+ timeout: 10000,
+ });
+ await expect(view.locator('[data-testid="follow-button"]')).toHaveCount(0);
+ });
});
diff --git a/tests/unit/specs/components/manage-list-members-dialog.test.js b/tests/unit/specs/components/manage-list-members-dialog.test.js
new file mode 100644
index 00000000..a53a31c2
--- /dev/null
+++ b/tests/unit/specs/components/manage-list-members-dialog.test.js
@@ -0,0 +1,379 @@
+import { describe, it, beforeEach, afterEach, mock } from "node:test";
+import assert from "node:assert/strict";
+import { Signal, SignalMap, ComputedMap } from "/js/signals.js";
+import "/js/components/manage-list-members-dialog.js";
+
+describe("manage-list-members-dialog", () => {
+ const originalSetTimeout = globalThis.setTimeout;
+
+ beforeEach(() => {
+ document.body.innerHTML = "";
+ globalThis.setTimeout = (fn) => originalSetTimeout(fn, 0);
+ });
+
+ afterEach(() => {
+ globalThis.setTimeout = originalSetTimeout;
+ });
+
+ async function nextFrame() {
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ }
+
+ async function flushMicrotasks() {
+ for (let i = 0; i < 10; i++) {
+ await Promise.resolve();
+ }
+ }
+
+ const LIST = {
+ uri: "at://did:plc:me/app.bsky.graph.list/rk1",
+ cid: "cid1",
+ name: "My List",
+ purpose: "app.bsky.graph.defs#curatelist",
+ creator: { did: "did:plc:me", handle: "me.test" },
+ };
+
+ function createProfile({ did, handle, displayName } = {}) {
+ return {
+ did,
+ handle,
+ displayName: displayName ?? handle,
+ avatar: "",
+ labels: [],
+ viewer: {},
+ };
+ }
+
+ function createFakeDataLayer({
+ members = [],
+ searchResults = null,
+ follows = null,
+ addFailure = null,
+ removeFailure = null,
+ membersPages = null,
+ } = {}) {
+ const $listMembers = new SignalMap();
+ const $currentUser = new Signal.State({
+ did: "did:plc:me",
+ handle: "me.test",
+ });
+ const $profileFollows = new SignalMap();
+ const $chatRecipientSearchResults = new Signal.State(null);
+ const $loading = new SignalMap();
+ const $errors = new SignalMap();
+ const addCalls = [];
+ const removeCalls = [];
+ const loadMembersCalls = [];
+
+ if (members.length && !membersPages) {
+ $listMembers.set(LIST.uri, {
+ items: members.map((p, i) => ({
+ uri: `at://did:plc:me/app.bsky.graph.listitem/li${i}`,
+ subject: p,
+ })),
+ cursor: null,
+ });
+ }
+ if (follows) {
+ $profileFollows.set("did:plc:me", { follows });
+ }
+ if (searchResults) {
+ $chatRecipientSearchResults.set(searchResults);
+ }
+
+ const dataLayer = {
+ dataStore: { $listMembers },
+ derived: {
+ $currentUser,
+ $profileFollows,
+ $chatRecipientSearchResults,
+ },
+ requests: {
+ statusStore: {
+ $statuses: new ComputedMap((requestId) => ({
+ loading: $loading.get(requestId) ?? false,
+ error: $errors.get(requestId) ?? null,
+ })),
+ },
+ loadListMembers: async (uri, { reload = false } = {}) => {
+ loadMembersCalls.push({ uri, reload });
+ if (membersPages) {
+ const idx = loadMembersCalls.length - 1;
+ const page = membersPages[idx];
+ if (!page) return;
+ const existing = $listMembers.get(uri);
+ const items = [
+ ...(existing && !reload ? existing.items : []),
+ ...page.items,
+ ];
+ $listMembers.set(uri, { items, cursor: page.cursor });
+ }
+ },
+ loadChatRecipientSearch: (query) => {
+ if (!query) {
+ $chatRecipientSearchResults.set(null);
+ }
+ return Promise.resolve();
+ },
+ },
+ declarative: {
+ ensureCurrentUser: async () => $currentUser.get(),
+ ensureProfileFollows: async () => $profileFollows.get("did:plc:me"),
+ },
+ mutations: {
+ addProfileToList: async (profile, list) => {
+ addCalls.push({ did: profile.did, listUri: list.uri });
+ if (addFailure) throw addFailure;
+ const existing = $listMembers.get(list.uri) ?? {
+ items: [],
+ cursor: null,
+ };
+ $listMembers.set(list.uri, {
+ ...existing,
+ items: [
+ {
+ uri: `at://did:plc:me/app.bsky.graph.listitem/new-${profile.did}`,
+ subject: profile,
+ },
+ ...existing.items,
+ ],
+ });
+ },
+ removeProfileFromList: async (profile, list, membershipUri) => {
+ removeCalls.push({ did: profile.did, membershipUri });
+ if (removeFailure) throw removeFailure;
+ const existing = $listMembers.get(list.uri);
+ if (existing) {
+ $listMembers.set(list.uri, {
+ ...existing,
+ items: existing.items.filter(
+ (item) => item.subject.did !== profile.did,
+ ),
+ });
+ }
+ },
+ },
+ };
+ return {
+ dataLayer,
+ $listMembers,
+ $profileFollows,
+ $chatRecipientSearchResults,
+ addCalls,
+ removeCalls,
+ loadMembersCalls,
+ };
+ }
+
+ function createDialog(dataLayer, list = LIST) {
+ const element = document.createElement("manage-list-members-dialog");
+ element.dataLayer = dataLayer;
+ element.list = list;
+ document.body.appendChild(element);
+ return element;
+ }
+
+ async function typeQuery(element, value) {
+ const input = element.querySelector(
+ '[data-testid="manage-list-members-search-input"]',
+ );
+ input.value = value;
+ input.dispatchEvent(new window.InputEvent("input", { bubbles: true }));
+ await nextFrame();
+ await nextFrame();
+ }
+
+ it("renders the dialog title and search input", () => {
+ const { dataLayer } = createFakeDataLayer();
+ const element = createDialog(dataLayer);
+ const dialog = element.querySelector("dialog.manage-list-members-dialog");
+ assert(dialog !== null);
+ assert(dialog.classList.contains("bottom-sheet"));
+ assert(element.textContent.includes("Add people to list"));
+ assert(
+ element.querySelector(
+ '[data-testid="manage-list-members-search-input"]',
+ ) !== null,
+ );
+ });
+
+ it("loads all list members on connect (paginating up to the cap)", async () => {
+ const alice = createProfile({ did: "did:plc:alice", handle: "alice.test" });
+ const bob = createProfile({ did: "did:plc:bob", handle: "bob.test" });
+ const { dataLayer, loadMembersCalls } = createFakeDataLayer({
+ membersPages: [
+ { items: [{ uri: "at://li1", subject: alice }], cursor: "next" },
+ { items: [{ uri: "at://li2", subject: bob }], cursor: null },
+ ],
+ });
+ createDialog(dataLayer);
+ await flushMicrotasks();
+ assert.deepEqual(loadMembersCalls.length, 2);
+ assert.deepEqual(loadMembersCalls[0].reload, true);
+ assert.deepEqual(loadMembersCalls[1].reload, false);
+ });
+
+ it("shows Add for a suggested profile that is not a member", async () => {
+ const alice = createProfile({ did: "did:plc:alice", handle: "alice.test" });
+ const { dataLayer } = createFakeDataLayer({
+ follows: [alice],
+ });
+ const element = createDialog(dataLayer);
+ await flushMicrotasks();
+ await nextFrame();
+ const buttons = element.querySelectorAll(
+ '[data-testid="manage-list-members-toggle"]',
+ );
+ assert.deepEqual(buttons.length, 1);
+ assert.deepEqual(buttons[0].dataset.teststate, "not-member");
+ assert.equal(buttons[0].textContent.trim(), "Add");
+ });
+
+ it("shows Remove for a search result that is already a member", async () => {
+ const alice = createProfile({ did: "did:plc:alice", handle: "alice.test" });
+ const { dataLayer } = createFakeDataLayer({
+ members: [alice],
+ searchResults: [alice],
+ });
+ const element = createDialog(dataLayer);
+ await flushMicrotasks();
+ await typeQuery(element, "alice");
+ const buttons = element.querySelectorAll(
+ '[data-testid="manage-list-members-toggle"]',
+ );
+ assert.deepEqual(buttons.length, 1);
+ assert.deepEqual(buttons[0].dataset.teststate, "member");
+ assert.equal(buttons[0].textContent.trim(), "Remove");
+ });
+
+ it("adds a profile when Add is clicked and flips the button to Remove", async () => {
+ const alice = createProfile({ did: "did:plc:alice", handle: "alice.test" });
+ const { dataLayer, addCalls } = createFakeDataLayer({
+ follows: [alice],
+ });
+ const element = createDialog(dataLayer);
+ await flushMicrotasks();
+ await nextFrame();
+ element.querySelector('[data-testid="manage-list-members-toggle"]').click();
+ await flushMicrotasks();
+ await nextFrame();
+ assert.deepEqual(addCalls.length, 1);
+ assert.deepEqual(addCalls[0].did, "did:plc:alice");
+ const button = element.querySelector(
+ '[data-testid="manage-list-members-toggle"]',
+ );
+ assert.deepEqual(button.dataset.teststate, "member");
+ assert.equal(button.textContent.trim(), "Remove");
+ });
+
+ it("removes a profile when Remove is clicked and flips the button to Add", async () => {
+ const alice = createProfile({ did: "did:plc:alice", handle: "alice.test" });
+ const { dataLayer, removeCalls } = createFakeDataLayer({
+ members: [alice],
+ follows: [alice],
+ });
+ const element = createDialog(dataLayer);
+ await flushMicrotasks();
+ await nextFrame();
+ element.querySelector('[data-testid="manage-list-members-toggle"]').click();
+ await flushMicrotasks();
+ await nextFrame();
+ assert.deepEqual(removeCalls.length, 1);
+ assert.deepEqual(removeCalls[0].did, "did:plc:alice");
+ assert.match(removeCalls[0].membershipUri, /listitem\/li0$/);
+ const button = element.querySelector(
+ '[data-testid="manage-list-members-toggle"]',
+ );
+ assert.deepEqual(button.dataset.teststate, "not-member");
+ assert.equal(button.textContent.trim(), "Add");
+ });
+
+ it("shows a spinner and disables the button while a toggle is pending", async () => {
+ const alice = createProfile({ did: "did:plc:alice", handle: "alice.test" });
+ let releaseAdd;
+ const gate = new Promise((resolve) => (releaseAdd = resolve));
+ const { dataLayer } = createFakeDataLayer({
+ follows: [alice],
+ });
+ dataLayer.mutations.addProfileToList = async () => {
+ await gate;
+ };
+ const element = createDialog(dataLayer);
+ await flushMicrotasks();
+ await nextFrame();
+ element.querySelector('[data-testid="manage-list-members-toggle"]').click();
+ await nextFrame();
+ const pendingButton = element.querySelector(
+ '[data-testid="manage-list-members-toggle"]',
+ );
+ assert(pendingButton.disabled);
+ assert(
+ pendingButton.querySelector('[data-testid="loading-spinner"]') !== null,
+ );
+ releaseAdd();
+ await flushMicrotasks();
+ await nextFrame();
+ const finalButton = element.querySelector(
+ '[data-testid="manage-list-members-toggle"]',
+ );
+ assert.equal(finalButton.disabled, false);
+ assert.equal(
+ finalButton.querySelector('[data-testid="loading-spinner"]'),
+ null,
+ );
+ });
+
+ it("switches from suggestions to search results when a query is typed", async () => {
+ const alice = createProfile({ did: "did:plc:alice", handle: "alice.test" });
+ const dan = createProfile({ did: "did:plc:dan", handle: "dan.test" });
+ const { dataLayer, $chatRecipientSearchResults } = createFakeDataLayer({
+ follows: [alice],
+ });
+ const element = createDialog(dataLayer);
+ await flushMicrotasks();
+ await nextFrame();
+ assert(
+ element.querySelector(
+ '[data-testid="manage-list-members-suggested-header"]',
+ ) !== null,
+ );
+ $chatRecipientSearchResults.set([dan]);
+ await typeQuery(element, "dan");
+ assert.equal(
+ element.querySelector(
+ '[data-testid="manage-list-members-suggested-header"]',
+ ),
+ null,
+ );
+ const rows = element.querySelectorAll(
+ '[data-testid="profile-list-item-body"]',
+ );
+ assert.deepEqual(rows.length, 1);
+ assert(rows[0].textContent.includes("@dan.test"));
+ });
+
+ it("shows the empty state when the search returns nothing", async () => {
+ const { dataLayer, $chatRecipientSearchResults } = createFakeDataLayer();
+ const element = createDialog(dataLayer);
+ $chatRecipientSearchResults.set([]);
+ await typeQuery(element, "zzz");
+ const emptyMessage = element.querySelector(
+ '[data-testid="feed-end-message"]',
+ );
+ assert(emptyMessage !== null);
+ assert.equal(emptyMessage.textContent.trim(), "No results");
+ });
+
+ it("closes on the close button", async () => {
+ const { dataLayer } = createFakeDataLayer();
+ const element = createDialog(dataLayer);
+ let closed = false;
+ element.addEventListener("dialog-closed", () => {
+ closed = true;
+ });
+ element.open();
+ element.querySelector('[data-testid="manage-list-members-close"]').click();
+ await flushMicrotasks();
+ assert(closed);
+ });
+});
diff --git a/tests/unit/specs/components/new-chat-dialog.test.js b/tests/unit/specs/components/new-chat-dialog.test.js
index 9daf68eb..4cf37d4c 100644
--- a/tests/unit/specs/components/new-chat-dialog.test.js
+++ b/tests/unit/specs/components/new-chat-dialog.test.js
@@ -202,10 +202,12 @@ describe("new-chat-dialog", () => {
$profileFollows.set("did:plc:me", { follows: [] });
const element = createDialog(dataLayer);
assert(
- element.querySelector('[data-testid="new-chat-empty-prompt"]') !== null,
+ [...element.querySelectorAll('[data-testid="feed-end-message"]')].some(
+ (el) => el.textContent.includes("Search for someone to message"),
+ ),
);
assert.deepEqual(
- element.querySelector('[data-testid="new-chat-result"]'),
+ element.querySelector('[data-testid="profile-list-item-button"]'),
null,
);
});
@@ -232,10 +234,7 @@ describe("new-chat-dialog", () => {
it("should show skeletons while follows load", () => {
const { dataLayer } = createFakeDataLayer();
const element = createDialog(dataLayer);
- assert(
- element.querySelectorAll('[data-testid="new-chat-skeleton"]').length >
- 0,
- );
+ assert(element.querySelectorAll(".profile-skeleton").length > 0);
});
it("should show only messageable follows under a header", async () => {
@@ -259,9 +258,11 @@ describe("new-chat-dialog", () => {
element.querySelector('[data-testid="new-chat-suggested-header"]') !==
null,
);
- const rows = element.querySelectorAll('[data-testid="new-chat-result"]');
+ const rows = element.querySelectorAll(
+ '[data-testid="profile-list-item-button"]',
+ );
assert.deepEqual(rows.length, 1);
- assert.deepEqual(rows[0].dataset.teststate, "messageable");
+ assert.deepEqual(rows[0].dataset.teststate, "enabled");
assert(rows[0].textContent.includes("@alice.test"));
});
@@ -278,10 +279,12 @@ describe("new-chat-dialog", () => {
});
const element = createDialog(dataLayer);
assert(
- element.querySelector('[data-testid="new-chat-empty-prompt"]') !== null,
+ [...element.querySelectorAll('[data-testid="feed-end-message"]')].some(
+ (el) => el.textContent.includes("Search for someone to message"),
+ ),
);
assert.deepEqual(
- element.querySelector('[data-testid="new-chat-result"]'),
+ element.querySelector('[data-testid="profile-list-item-button"]'),
null,
);
});
@@ -294,7 +297,9 @@ describe("new-chat-dialog", () => {
await flushMicrotasks();
await nextFrame();
assert(
- element.querySelector('[data-testid="new-chat-empty-prompt"]') !== null,
+ [...element.querySelectorAll('[data-testid="feed-end-message"]')].some(
+ (el) => el.textContent.includes("Search for someone to message"),
+ ),
);
});
@@ -306,12 +311,11 @@ describe("new-chat-dialog", () => {
await flushMicrotasks();
await nextFrame();
assert(
- element.querySelector('[data-testid="new-chat-empty-prompt"]') !== null,
- );
- assert.deepEqual(
- element.querySelectorAll('[data-testid="new-chat-skeleton"]').length,
- 0,
+ [...element.querySelectorAll('[data-testid="feed-end-message"]')].some(
+ (el) => el.textContent.includes("Search for someone to message"),
+ ),
);
+ assert.deepEqual(element.querySelectorAll(".profile-skeleton").length, 0);
});
it("should hide the suggestions once a query is typed", async () => {
@@ -338,7 +342,9 @@ describe("new-chat-dialog", () => {
element.querySelector('[data-testid="new-chat-suggested-header"]'),
null,
);
- const rows = element.querySelectorAll('[data-testid="new-chat-result"]');
+ const rows = element.querySelectorAll(
+ '[data-testid="profile-list-item-button"]',
+ );
assert.deepEqual(rows.length, 1);
assert(rows[0].textContent.includes("@dan.test"));
});
@@ -380,7 +386,9 @@ describe("new-chat-dialog", () => {
assert.deepEqual(input.value, "");
assert.deepEqual(searchCalls[searchCalls.length - 1].query, "");
assert(
- element.querySelector('[data-testid="new-chat-empty-prompt"]') !== null,
+ [...element.querySelectorAll('[data-testid="feed-end-message"]')].some(
+ (el) => el.textContent.includes("Search for someone to message"),
+ ),
);
assert.deepEqual(
element.querySelector('[data-testid="new-chat-search-clear"]'),
@@ -412,10 +420,7 @@ describe("new-chat-dialog", () => {
const element = createDialog(dataLayer);
$loading.set("loadChatRecipientSearch", true);
await typeQuery(element, "alice");
- assert(
- element.querySelectorAll('[data-testid="new-chat-skeleton"]').length >
- 0,
- );
+ assert(element.querySelectorAll(".profile-skeleton").length > 0);
});
it("should show the empty state when a settled search has no results", async () => {
@@ -423,7 +428,11 @@ describe("new-chat-dialog", () => {
const element = createDialog(dataLayer);
$results.set([]);
await typeQuery(element, "alice");
- assert(element.querySelector('[data-testid="empty-state"]') !== null);
+ assert(
+ [...element.querySelectorAll('[data-testid="feed-end-message"]')].some(
+ (el) => el.textContent.includes("No results"),
+ ),
+ );
});
it("should show an error row when the search fails with an ApiError", async () => {
@@ -495,7 +504,9 @@ describe("new-chat-dialog", () => {
createProfile({ did: "did:plc:me", handle: "me.test" }),
]);
await typeQuery(element, "test");
- const rows = element.querySelectorAll('[data-testid="new-chat-result"]');
+ const rows = element.querySelectorAll(
+ '[data-testid="profile-list-item-button"]',
+ );
assert.deepEqual(rows.length, 1);
assert(rows[0].textContent.includes("@alice.test"));
});
@@ -516,13 +527,16 @@ describe("new-chat-dialog", () => {
}),
]);
await typeQuery(element, "test");
- const rows = element.querySelectorAll('[data-testid="new-chat-result"]');
+ const rows = element.querySelectorAll(
+ '[data-testid="profile-list-item-button"]',
+ );
assert.deepEqual(rows.length, 2);
- assert.deepEqual(rows[0].dataset.teststate, "messageable");
+ assert.deepEqual(rows[0].dataset.teststate, "enabled");
assert.deepEqual(rows[0].disabled, false);
- assert.deepEqual(rows[1].dataset.teststate, "not-messageable");
+ assert.deepEqual(rows[1].dataset.teststate, "disabled");
assert.deepEqual(rows[1].disabled, true);
- assert(rows[1].textContent.includes("@carol.test can't be messaged"));
+ assert(rows[1].textContent.includes("@carol.test"));
+ assert(rows[1].textContent.includes("Can't be messaged"));
});
it("should treat a missing allowIncoming declaration as following-only", async () => {
@@ -537,10 +551,12 @@ describe("new-chat-dialog", () => {
createProfile({ did: "did:plc:stranger", handle: "stranger.test" }),
]);
await typeQuery(element, "test");
- const rows = element.querySelectorAll('[data-testid="new-chat-result"]');
- assert.deepEqual(rows[0].dataset.teststate, "messageable");
+ const rows = element.querySelectorAll(
+ '[data-testid="profile-list-item-button"]',
+ );
+ assert.deepEqual(rows[0].dataset.teststate, "enabled");
assert(rows[0].textContent.includes("@follower.test"));
- assert.deepEqual(rows[1].dataset.teststate, "not-messageable");
+ assert.deepEqual(rows[1].dataset.teststate, "disabled");
});
});
@@ -561,7 +577,7 @@ describe("new-chat-dialog", () => {
}),
]);
await typeQuery(element, "alice");
- element.querySelector('[data-testid="new-chat-result"]').click();
+ element.querySelector('[data-testid="profile-list-item-button"]').click();
await flushMicrotasks();
assert(closed, "the dialog closes when a user is selected");
assert.deepEqual(ensureCalls, ["did:plc:alice"]);
@@ -587,7 +603,7 @@ describe("new-chat-dialog", () => {
}),
]);
await typeQuery(element, "alice");
- element.querySelector('[data-testid="new-chat-result"]').click();
+ element.querySelector('[data-testid="profile-list-item-button"]').click();
await flushMicrotasks();
const toast = document.body.querySelector('[data-testid="toast"]');
assert(toast !== null, "toast should be shown");
@@ -614,7 +630,7 @@ describe("new-chat-dialog", () => {
}),
]);
await typeQuery(element, "alice");
- element.querySelector('[data-testid="new-chat-result"]').click();
+ element.querySelector('[data-testid="profile-list-item-button"]').click();
await flushMicrotasks();
const toast = document.body.querySelector('[data-testid="toast"]');
assert(toast !== null, "toast should be shown");
@@ -636,7 +652,7 @@ describe("new-chat-dialog", () => {
}),
]);
await typeQuery(element, "alice");
- element.querySelector('[data-testid="new-chat-result"]').click();
+ element.querySelector('[data-testid="profile-list-item-button"]').click();
await flushMicrotasks();
const toast = document.body.querySelector('[data-testid="toast"]');
assert(toast !== null, "toast should be shown");
diff --git a/tests/unit/specs/components/plugin-profiles-list.test.js b/tests/unit/specs/components/plugin-profiles-list.test.js
index 67f240ac..363f6674 100644
--- a/tests/unit/specs/components/plugin-profiles-list.test.js
+++ b/tests/unit/specs/components/plugin-profiles-list.test.js
@@ -89,6 +89,21 @@ describe("plugin-profiles-list", () => {
);
});
+ it("does not render follow buttons on any row", async () => {
+ const dataLayer = makeDataLayer();
+ dataLayer.__setProfile("did:test:a", makeProfile("did:test:a", "a.test"));
+ dataLayer.__setProfile("did:test:b", makeProfile("did:test:b", "b.test"));
+ const element = document.createElement("plugin-profiles-list");
+ element.dataLayer = dataLayer;
+ element.setAttribute("dids", "did:test:a,did:test:b");
+ document.body.appendChild(element);
+ await flushMicrotasks();
+ assert.deepEqual(
+ element.querySelectorAll("[data-testid='follow-button']").length,
+ 0,
+ );
+ });
+
it("does not render the end-of-feed message", async () => {
const dataLayer = makeDataLayer();
dataLayer.__setProfile("did:test:a", makeProfile("did:test:a", "a.test"));
diff --git a/tests/unit/specs/templates/profileFeed.template.test.js b/tests/unit/specs/templates/profileFeed.template.test.js
index a51309c3..e89fed08 100644
--- a/tests/unit/specs/templates/profileFeed.template.test.js
+++ b/tests/unit/specs/templates/profileFeed.template.test.js
@@ -189,10 +189,10 @@ describe("profileListItemTemplate - displayName sanitization", () => {
});
describe("profileListItemTemplate - custom badge", () => {
- it("should render the badge returned by badgeTemplate", () => {
+ it("should render the badge returned by rightItemTemplate", () => {
const result = profileListItemTemplate({
actor: mockActor,
- badgeTemplate: (actor) =>
+ rightItemTemplate: (actor) =>
html`${actor.handle}
`,
});
const container = document.createElement("div");
@@ -202,10 +202,10 @@ describe("profileListItemTemplate - custom badge", () => {
assert(badge.textContent.includes(mockActor.handle));
});
- it("should render nothing when badgeTemplate returns null", () => {
+ it("should render nothing when rightItemTemplate returns null", () => {
const result = profileListItemTemplate({
actor: mockActor,
- badgeTemplate: () => null,
+ rightItemTemplate: () => null,
});
const container = document.createElement("div");
render(result, container);
@@ -309,11 +309,12 @@ describe("profileFeedTemplate", () => {
assert(msg.textContent.includes("End of feed"));
});
- it("should pass badgeTemplate through to list items", () => {
+ it("should pass rightItemTemplate through to list items", () => {
const result = profileFeedTemplate({
profiles: [mockActor],
hasMore: false,
- badgeTemplate: () => html`Admin
`,
+ rightItemTemplate: () =>
+ html`Admin
`,
});
render(result, container);
assert(container.querySelector("[data-testid='custom-badge']") !== null);