diff --git a/package.json b/package.json index 43a6793d..b2218502 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.18.65", + "version": "0.18.66", "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 6e415d35..aca8a359 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -1583,7 +1583,7 @@ button.sidebar-plugin-nav-item { } .feed-end-message { - padding: 32px 16px 0; + padding: 32px 16px; color: var(--text-color-muted); font-size: 14px; text-align: center; @@ -5869,6 +5869,13 @@ button.profile-list-item-button.is-disabled { padding: 32px 0; } +.list-empty-add-people-button { + display: flex; + align-items: center; + gap: 6px; + margin: 32px auto 0; +} + .feeds-list-item .pin-feed-button svg { width: 16px; height: 16px; @@ -9124,17 +9131,17 @@ toggle-switch { } @media (max-width: 799px) { - .new-chat-dialog { + .search-dialog { display: flex; flex-direction: column; } - .new-chat-dialog-content { + .search-dialog-content { flex: 1; min-height: 0; } - .new-chat-results { + .search-dialog-results { flex: 1; height: auto; min-height: 0; @@ -9142,12 +9149,14 @@ toggle-switch { } @media (min-width: 800px) { - .new-chat-dialog { + .search-dialog { max-width: 480px; + top: 50%; + max-height: calc(100vh - var(--sheet-top)); } } -.new-chat-dialog-content { +.search-dialog-content { display: flex; flex-direction: column; gap: 16px; @@ -9157,24 +9166,25 @@ toggle-switch { } @media (min-width: 800px) { - .new-chat-dialog-content { + .search-dialog-content { padding-top: 24px; + max-height: 80vh; } } -.new-chat-dialog-header { +.search-dialog-header { display: flex; align-items: center; justify-content: space-between; } -.new-chat-dialog-title { +.search-dialog-title { font-size: 1.3rem; font-weight: 700; margin: 0; } -.new-chat-dialog-close { +.search-dialog-close { display: none; background: none; border: none; @@ -9185,25 +9195,25 @@ toggle-switch { } @media (min-width: 800px) { - .new-chat-dialog-close { + .search-dialog-close { display: flex; align-items: center; } } @media (hover: hover) { - .new-chat-dialog-close:hover { + .search-dialog-close:hover { opacity: 1; } } -.new-chat-search-container { +.search-dialog-input-container { position: relative; display: flex; flex-direction: column; } -.new-chat-search-container .search-icon { +.search-dialog-input-container .search-icon { position: absolute; left: 10px; top: 50%; @@ -9214,13 +9224,13 @@ toggle-switch { pointer-events: none; } -.new-chat-search-container .search-clear-button { +.search-dialog-input-container .search-clear-button { right: 8px; top: 50%; transform: translateY(-50%); } -.new-chat-search-input { +.search-dialog-input { padding: 12px; padding-left: 42px; padding-right: 36px; @@ -9232,19 +9242,25 @@ toggle-switch { outline: none; } -.new-chat-results { +.search-dialog-results { display: flex; flex-direction: column; height: 50vh; overflow-y: auto; } -.new-chat-message-row { +.search-dialog-message { color: var(--text-color-secondary); text-align: center; padding: 20px 0; } +.search-dialog-section-header { + color: var(--text-color-secondary); + font-size: 0.85rem; + padding: 4px 4px 8px; +} + .new-chat-not-messageable-hint { align-self: center; color: var(--text-color-muted); @@ -9252,12 +9268,6 @@ toggle-switch { flex-shrink: 0; } -.new-chat-section-header { - color: var(--text-color-secondary); - font-size: 0.85rem; - padding: 4px 4px 8px; -} - .new-chat-result { display: flex; align-items: center; @@ -9332,18 +9342,6 @@ toggle-switch { 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%; } @@ -10233,7 +10231,7 @@ p.setting-item-desc { max-width: 410px; } .bottom-sheet.text-modal.compact { - max-width: 320px; + max-width: 350px; } .bottom-sheet.text-modal .modal-dialog-content { padding-top: 24px; diff --git a/src/js/components/manage-list-members-dialog.js b/src/js/components/manage-list-members-dialog.js index 2e926eea..a216bbaf 100644 --- a/src/js/components/manage-list-members-dialog.js +++ b/src/js/components/manage-list-members-dialog.js @@ -105,7 +105,7 @@ class ManageListMembersDialog extends Component { } else { this.dataLayer.requests.loadChatRecipientSearch(query, { limit: 12 }); } - const results = this.querySelector(".new-chat-results"); + const results = this.querySelector(".search-dialog-results"); if (results) results.scrollTop = 0; } @@ -176,7 +176,7 @@ class ManageListMembersDialog extends Component { render( html` { @@ -194,11 +194,11 @@ class ManageListMembersDialog extends Component { this.dispatchEvent(new CustomEvent("dialog-closed")); }} > -
-
-

Add people to list

+
+
+

Add people to list

-
+
${searchIconTemplate()} -
+
${(() => { if (query) { if (searchStatus?.error) { return html`
We're having network issues, try again @@ -265,7 +265,7 @@ class ManageListMembersDialog extends Component { } return html`
Suggested @@ -289,15 +289,15 @@ class ManageListMembersDialog extends Component { const dialog = this.querySelector(".manage-list-members-dialog"); if (dialog?.open) return; dialog.showModal(); - this.querySelector(".new-chat-search-input")?.focus({ + this.querySelector(".search-dialog-input")?.focus({ preventScroll: true, }); enableDragToDismiss(dialog, { onClose: () => this.close(), - scrollContainer: this.querySelector(".new-chat-results"), + scrollContainer: this.querySelector(".search-dialog-results"), ignoreTouchTarget: (element) => element.closest("button, input") !== null, }); - resetScrollOnBlur(dialog, this.querySelector(".new-chat-results")); + resetScrollOnBlur(dialog, this.querySelector(".search-dialog-results")); } close() { diff --git a/src/js/components/new-chat-dialog.js b/src/js/components/new-chat-dialog.js index 70caae36..9c5f8aba 100644 --- a/src/js/components/new-chat-dialog.js +++ b/src/js/components/new-chat-dialog.js @@ -125,7 +125,7 @@ class NewChatDialog extends Component { } else { this.dataLayer.requests.loadChatRecipientSearch(query, { limit: 12 }); } - const results = this.querySelector(".new-chat-results"); + const results = this.querySelector(".search-dialog-results"); if (results) results.scrollTop = 0; } @@ -164,7 +164,7 @@ class NewChatDialog extends Component { render( html` { @@ -182,11 +182,11 @@ class NewChatDialog extends Component { this.dispatchEvent(new CustomEvent("dialog-closed")); }} > -
-
-

Start a new chat

+
+
+

Start a new chat

-
+
${searchIconTemplate()} -
+
${(() => { if (query) { if (searchStatus?.error) { return html`
We're having network issues, try again @@ -258,7 +258,7 @@ class NewChatDialog extends Component { } return html`
Suggested @@ -282,15 +282,15 @@ class NewChatDialog extends Component { const dialog = this.querySelector(".new-chat-dialog"); if (dialog?.open) return; dialog.showModal(); - this.querySelector(".new-chat-search-input")?.focus({ + this.querySelector(".search-dialog-input")?.focus({ preventScroll: true, }); enableDragToDismiss(dialog, { onClose: () => this.close(), - scrollContainer: this.querySelector(".new-chat-results"), + scrollContainer: this.querySelector(".search-dialog-results"), ignoreTouchTarget: (element) => element.closest("button, input") !== null, }); - resetScrollOnBlur(dialog, this.querySelector(".new-chat-results")); + resetScrollOnBlur(dialog, this.querySelector(".search-dialog-results")); } close() { diff --git a/src/js/templates/profileFeed.template.js b/src/js/templates/profileFeed.template.js index 28f5307c..f775c3d4 100644 --- a/src/js/templates/profileFeed.template.js +++ b/src/js/templates/profileFeed.template.js @@ -149,7 +149,7 @@ export function profileListItemTemplate({
${rightItem}
- ${showsFollowsYou + ${showsFollowsYou && !compact ? html`
Follows you
` diff --git a/src/js/views/listDetail.view.js b/src/js/views/listDetail.view.js index fd321473..c15426a3 100644 --- a/src/js/views/listDetail.view.js +++ b/src/js/views/listDetail.view.js @@ -9,6 +9,7 @@ import { auth } from "/js/auth.js"; import { headerTemplate } from "/js/templates/header.template.js"; import "/js/components/tab-bar.js"; import { pinIconTemplate } from "/js/templates/icons/pinIcon.template.js"; +import { plusIconTemplate } from "/js/templates/icons/plusIcon.template.js"; import { richTextTemplate } from "/js/templates/richText.template.js"; import { pageEffect } from "/js/router.js"; import { FEED_PAGE_SIZE } from "/js/config.js"; @@ -119,6 +120,7 @@ class ListDetailView extends View { const hasMoreMembers = membersEntry?.cursor != null; const activeTab = state.$activeTab.get(); const isCurateList = !isModerationList(list); + const isCurrentUserList = listCreator?.did === currentUser?.did; const listPermalink = `https://bsky.app/profile/${listCreatorHandle || handleOrDid}/lists/${rkey}`; render( html`
@@ -173,9 +175,7 @@ class ListDetailView extends View { > Copy link to list - ${listCreator?.did && - currentUser?.did && - listCreator.did === currentUser.did + ${isCurrentUserList ? html` handleAddPeople(list)} @@ -238,7 +238,7 @@ class ListDetailView extends View { > ${isModerationList(list) ? "Moderation list" : "List"} by - ${listCreator.did === currentUser?.did + ${isCurrentUserList ? "you" : `@${listCreator.handle}`}
` @@ -290,8 +290,17 @@ class ListDetailView extends View { pluginService, showEndMessage: true, })} + ${feed?.feed?.length === 0 && isCurrentUserList + ? html`` + : ""}
` - : html`
+ : html`
${profileFeedTemplate({ profiles: members, hasMore: hasMoreMembers, @@ -303,6 +312,15 @@ class ListDetailView extends View { profileInteractionHandler, ...(isCurateList ? {} : { rightItemTemplate: null }), })} + ${members?.length === 0 && isCurrentUserList + ? html`` + : ""}
`}
`} diff --git a/src/js/views/search.view.js b/src/js/views/search.view.js index d6a2eb88..93c82e30 100644 --- a/src/js/views/search.view.js +++ b/src/js/views/search.view.js @@ -523,16 +523,24 @@ class SearchView extends View { handleInput(event.target.value)} + @input=${(event) => { + // Prevent events from being picked up by password manager extensions + event.stopPropagation(); + handleInput(event.target.value); + }} @keydown=${(event) => { + event.stopPropagation(); if (event.key === "Enter") { event.preventDefault(); commitSearch(); diff --git a/tests/e2e/specs/views/listDetail.view.test.js b/tests/e2e/specs/views/listDetail.view.test.js index 8f753ed6..319ece16 100644 --- a/tests/e2e/specs/views/listDetail.view.test.js +++ b/tests/e2e/specs/views/listDetail.view.test.js @@ -129,6 +129,105 @@ test.describe("List Detail view", () => { }); }); + test("should show Add people button under empty members on own list", async ({ + page, + }) => { + const OWN_LIST_URI = "at://did:plc:testuser123/app.bsky.graph.list/ownlist"; + const mockServer = new MockServer(); + const list = createList({ + uri: OWN_LIST_URI, + name: "My Own List", + creatorHandle: "testuser.bsky.social", + }); + mockServer.addLists([list]); + await mockServer.setup(page); + + await login(page); + await page.goto("/profile/testuser.bsky.social/lists/ownlist"); + + const view = page.locator("#list-detail-view"); + await expect(view.locator('[data-testid="tab-people"]')).toBeVisible({ + timeout: 10000, + }); + await view.locator('[data-testid="tab-people"]').click(); + + await expect( + view.locator('[data-testid="list-empty-add-people-button"]'), + ).toBeVisible({ timeout: 10000 }); + await view.locator('[data-testid="list-empty-add-people-button"]').click(); + await expect( + page.locator('[data-testid="manage-list-members-dialog"]'), + ).toBeVisible({ timeout: 10000 }); + }); + + test("should show Add people button under empty feed on own list", async ({ + page, + }) => { + const OWN_LIST_URI = "at://did:plc:testuser123/app.bsky.graph.list/ownlist"; + const mockServer = new MockServer(); + const list = createList({ + uri: OWN_LIST_URI, + name: "My Own List", + creatorHandle: "testuser.bsky.social", + }); + mockServer.addLists([list]); + await mockServer.setup(page); + + await login(page); + await page.goto("/profile/testuser.bsky.social/lists/ownlist"); + + const view = page.locator("#list-detail-view"); + await expect( + view.locator('[data-testid="list-tab-content"]'), + ).toHaveAttribute("data-teststate", "posts", { timeout: 10000 }); + await expect( + view.locator('[data-testid="list-empty-add-people-button"]'), + ).toBeVisible({ timeout: 10000 }); + }); + + test("should not show Add people button on empty feed of another user's list", async ({ + page, + }) => { + const mockServer = new MockServer(); + setupList(mockServer); + await mockServer.setup(page); + + await login(page); + await page.goto("/profile/creator1.bsky.social/lists/mylist"); + + const view = page.locator("#list-detail-view"); + await expect(view.locator('[data-testid="feed-end-message"]')).toBeVisible({ + timeout: 10000, + }); + await expect( + view.locator('[data-testid="list-empty-add-people-button"]'), + ).toHaveCount(0); + }); + + test("should not show Add people button on empty members of another user's list", async ({ + page, + }) => { + const mockServer = new MockServer(); + setupList(mockServer); + await mockServer.setup(page); + + await login(page); + await page.goto("/profile/creator1.bsky.social/lists/mylist"); + + const view = page.locator("#list-detail-view"); + await expect(view.locator('[data-testid="tab-people"]')).toBeVisible({ + timeout: 10000, + }); + await view.locator('[data-testid="tab-people"]').click(); + + await expect(view.locator('[data-testid="feed-end-message"]')).toBeVisible({ + timeout: 10000, + }); + await expect( + view.locator('[data-testid="list-empty-add-people-button"]'), + ).toHaveCount(0); + }); + test("should show pin button as unpinned by default", async ({ page }) => { const mockServer = new MockServer(); setupList(mockServer); diff --git a/tests/unit/specs/components/new-chat-dialog.test.js b/tests/unit/specs/components/new-chat-dialog.test.js index 4cf37d4c..5d6b33fd 100644 --- a/tests/unit/specs/components/new-chat-dialog.test.js +++ b/tests/unit/specs/components/new-chat-dialog.test.js @@ -355,7 +355,7 @@ describe("new-chat-dialog", () => { const { dataLayer } = createFakeDataLayer(); const element = createDialog(dataLayer); assert( - element.querySelector(".new-chat-search-container .search-icon") !== + element.querySelector(".search-dialog-input-container .search-icon") !== null, ); }); @@ -692,7 +692,7 @@ describe("new-chat-dialog", () => { const element = createDialog(dataLayer); element.open(); - const results = element.querySelector(".new-chat-results"); + const results = element.querySelector(".search-dialog-results"); const input = element.querySelector( '[data-testid="new-chat-search-input"]', ); diff --git a/tests/unit/specs/templates/profileFeed.template.test.js b/tests/unit/specs/templates/profileFeed.template.test.js index e89fed08..7cb6bd45 100644 --- a/tests/unit/specs/templates/profileFeed.template.test.js +++ b/tests/unit/specs/templates/profileFeed.template.test.js @@ -54,6 +54,34 @@ describe("profileListItemTemplate", () => { }); }); +describe("profileListItemTemplate - follows-you badge", () => { + const followsBackActor = { + ...mockActor, + viewer: { followedBy: "at://did:plc:testuser/app.bsky.graph.follow/1" }, + }; + + it("should render follows-you badge by default", () => { + const result = profileListItemTemplate({ actor: followsBackActor }); + const container = document.createElement("div"); + render(result, container); + assert( + container.querySelector("[data-testid='follows-you-badge']") !== null, + ); + }); + + it("should hide follows-you badge when compact", () => { + const result = profileListItemTemplate({ + actor: followsBackActor, + compact: true, + }); + const container = document.createElement("div"); + render(result, container); + assert( + container.querySelector("[data-testid='follows-you-badge']") === null, + ); + }); +}); + describe("profileListItemTemplate - verification badge", () => { it("should render verification badge for verified actor", () => { const verifiedActor = {