diff --git a/package.json b/package.json index 7750a560..0019926d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.20.20", + "version": "0.20.21", "type": "module", "scripts": { "start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve", diff --git a/src/js/components/reactions-dialog.js b/src/js/components/reactions-dialog.js index a45edae5..4923a869 100644 --- a/src/js/components/reactions-dialog.js +++ b/src/js/components/reactions-dialog.js @@ -18,17 +18,17 @@ class ReactionsDialog extends Component { this._dispose = effect(() => { this.render(); }); - requestAnimationFrame(() => { - const dialog = this.querySelector(".reactions-dialog"); - if (dialog && !dialog.open) { - dialog.showModal(); - this.scrollLock ??= scrollLocks.acquire({ target: this }); - enableDragToDismiss(dialog, { - onDismiss: () => this._close(), - scrollContainer: this.querySelector(".reactions-list"), - ignoreTouchTarget: (element) => element.closest("button") !== null, - }); - } + } + + open() { + const dialog = this.querySelector(".reactions-dialog"); + if (!dialog || dialog.open) return; + dialog.showModal(); + this.scrollLock ??= scrollLocks.acquire({ target: this }); + enableDragToDismiss(dialog, { + onDismiss: () => this._close(), + scrollContainer: this.querySelector(".reactions-list"), + ignoreTouchTarget: (element) => element.closest("button") !== null, }); } @@ -39,7 +39,14 @@ class ReactionsDialog extends Component { } _close() { - return closeWithAnimation(this.querySelector(".reactions-dialog")); + const dialog = this.querySelector(".reactions-dialog"); + if (!dialog?.open) { + this.scrollLock?.release(); + this.scrollLock = null; + this.dispatchEvent(new CustomEvent("close")); + return Promise.resolve(); + } + return closeWithAnimation(dialog); } _getMessage() { @@ -89,7 +96,6 @@ class ReactionsDialog extends Component { const reactions = message?.reactions || []; if (reactions.length === 0) { - render(html``, this); this._close(); return; } diff --git a/src/js/views/chatDetail.view.js b/src/js/views/chatDetail.view.js index eb68d46e..e49e0ad8 100644 --- a/src/js/views/chatDetail.view.js +++ b/src/js/views/chatDetail.view.js @@ -89,7 +89,6 @@ export default async function chatDetailView({ state.$isMuteSaving = new Signal.State(false); state.$activeMessageId = new Signal.State(null); state.$paletteMessageId = new Signal.State(null); - state.$reactionsDialogMessageId = new Signal.State(null); state.$stagedReply = new Signal.State(null); // null | { url, record, status: "loading" | "ready" | "error" } state.$stagedRecordEmbed = new Signal.State(null); @@ -498,6 +497,21 @@ export default async function chatDetailView({ } } + function openReactionsDialog(messageId, currentUserDid) { + if (document.querySelector("reactions-dialog")) return; + const dialog = document.createElement("reactions-dialog"); + dialog.messageId = messageId; + dialog.convoId = convoId; + dialog.currentUserDid = currentUserDid; + dialog.dataLayer = dataLayer; + dialog.addEventListener("close", () => dialog.remove()); + dialog.addEventListener("remove-reaction", (event) => + handleReactionRemove(event.detail.emoji, messageId, currentUserDid), + ); + document.body.appendChild(dialog); + dialog.open(); + } + function handleMessageClick(messageId) { if (!isMobileViewport() && canHover()) { return; @@ -779,7 +793,7 @@ export default async function chatDetailView({ @click=${(e) => { if (!isGroup) return; e.stopPropagation(); - state.$reactionsDialogMessageId.set(message.id); + openReactionsDialog(message.id, currentUserDid); }} aria-label=${isGroup ? `${describeReactions()}. Tap to view reactions` @@ -1377,7 +1391,6 @@ export default async function chatDetailView({ const isLocked = !!groupDetails && groupDetails.lockStatus !== "unlocked"; const canReactNow = !!convo && convo.status !== "disabled" && !isLocked; const convoPermalink = getPermalinkForConvo(convoId); - const reactionsDialogMessageId = state.$reactionsDialogMessageId.get(); const stagedReply = state.$stagedReply.get(); const stagedRecordEmbed = state.$stagedRecordEmbed.get(); const stagedReplySenderProfile = @@ -1552,31 +1565,6 @@ export default async function chatDetailView({ `} - ${reactionsDialogMessageId - ? html` state.$reactionsDialogMessageId.set(null)} - @remove-reaction=${async (e) => { - const { emoji } = e.detail; - try { - await dataLayer.mutations.removeMessageReaction( - convoId, - reactionsDialogMessageId, - emoji, - currentUser?.did, - ); - } catch (error) { - console.error(error); - showToast("Failed to remove emoji reaction", { - style: "error", - }); - } - }} - >` - : ""} `, root, ); diff --git a/tests/e2e/specs/views/chatDetail.view.test.js b/tests/e2e/specs/views/chatDetail.view.test.js index bb543d8e..923e722b 100644 --- a/tests/e2e/specs/views/chatDetail.view.test.js +++ b/tests/e2e/specs/views/chatDetail.view.test.js @@ -2044,6 +2044,44 @@ test.describe("Chat detail view", () => { await expect(rows.first()).toHaveAttribute("data-teststate", "own"); }); + test("removing the last own reaction from the dialog closes and removes it", async ({ + page, + }) => { + const message = createMessage({ + id: "msg-1", + text: "Hello group", + senderDid: alice.did, + }); + message.reactions = [ + { + createdAt: "2025-01-15T12:05:00.000Z", + sender: { did: userProfile.did }, + value: "👍", + }, + ]; + const mockServer = setupGroupConvo({ messages: [message] }); + await mockServer.setup(page); + + await login(page); + await page.goto("/messages/group-1"); + + const chatDetailView = page.locator("#chat-detail-view"); + const reactionPill = chatDetailView.locator( + '[data-testid="message-reactions"]', + ); + await expect(reactionPill).toBeVisible({ timeout: 10000 }); + + await reactionPill.click(); + const dialog = page.locator('[data-testid="reactions-dialog"]'); + await expect(dialog).toBeVisible({ timeout: 5000 }); + + await dialog + .locator('[data-testid="reaction-row"][data-teststate="own"]') + .click(); + await expect(page.locator("reactions-dialog")).toHaveCount(0); + await expect(reactionPill).toHaveCount(0); + }); + test("caps visible reaction emojis at 10 and shows the total count", async ({ page, }) => {