···1414import "/js/components/context-menu-item.js";
1515import "/js/components/context-menu-item-group.js";
1616import { cameraIconTemplate } from "/js/templates/icons/cameraIcon.template.js";
1717-import { confirm } from "/js/modals.js";
1717+import { confirmModal } from "/js/modals/confirm.modal.js";
18181919const MAX_DISPLAY_NAME_LENGTH = 64;
2020const MAX_DESCRIPTION_LENGTH = 256;
···507507508508 async confirmClose() {
509509 if (!this._isDirty || !!this._croppingTarget || this._saving) return true;
510510- return confirm("Are you sure you want to discard your changes?", {
510510+ return confirmModal("Are you sure you want to discard your changes?", {
511511 title: "Discard changes?",
512512 confirmButtonStyle: "danger",
513513 confirmButtonText: "Discard",
+2-2
src/js/components/post-composer.js
···1111 sanitizeUri,
1212} from "/js/utils.js";
1313import { externalLinkTemplate } from "/js/templates/externalLink.template.js";
1414-import { confirm } from "/js/modals.js";
1414+import { confirmModal } from "/js/modals/confirm.modal.js";
1515import { ScrollLock } from "/js/scrollLock.js";
1616import { imageIconTemplate } from "/js/templates/icons/imageIcon.template.js";
1717import { emojiIconTemplate } from "/js/templates/icons/emojiIcon.template.js";
···881881 ) {
882882 return true;
883883 }
884884- return confirm("Are you sure you'd like to discard this draft?", {
884884+ return confirmModal("Are you sure you'd like to discard this draft?", {
885885 title: "Discard draft?",
886886 confirmButtonStyle: "danger",
887887 confirmButtonText: "Discard",
+81-17
src/js/groupChatLinkService.js
···11import { html } from "/js/lib/lit-html.js";
22-import { showActionModal, showInfoModal } from "/js/modals.js";
22+import { Modal } from "/js/modals/modal.js";
33+import { alertModal } from "/js/modals/alert.modal.js";
34import { showToast } from "/js/toasts.js";
4566+class JoinChatModal extends Modal {
77+ get className() {
88+ return "bottom-sheet text-modal join-chat-modal";
99+ }
1010+1111+ get attributes() {
1212+ return { "data-testid": "join-chat-modal" };
1313+ }
1414+1515+ constructor(options) {
1616+ super(options);
1717+ this.isPending = false;
1818+ }
1919+2020+ canDismiss() {
2121+ return !this.isPending;
2222+ }
2323+2424+ render({ dismiss, update, props: { preview, onSubmit } }) {
2525+ const name = preview.name ?? "";
2626+ const runConfirm = async () => {
2727+ if (this.isPending) return;
2828+ this.isPending = true;
2929+ update();
3030+ try {
3131+ await onSubmit();
3232+ this.isPending = false;
3333+ dismiss(true);
3434+ } catch {
3535+ this.isPending = false;
3636+ update();
3737+ }
3838+ };
3939+ return html`
4040+ <div class="modal-dialog-content">
4141+ <h2 class="modal-dialog-title" data-testid="modal-title">
4242+ ${preview.requireApproval ? "Request to join" : "Join group chat"}
4343+ </h2>
4444+ <p class="modal-dialog-message" data-testid="modal-message">
4545+ ${preview.requireApproval
4646+ ? html`Send a request to join <strong>${name}</strong>. The group
4747+ owner will review your request before you can see messages.`
4848+ : html`You're about to join <strong>${name}</strong>.`}
4949+ </p>
5050+ <div class="modal-dialog-buttons">
5151+ <button
5252+ class="modal-dialog-button cancel-button"
5353+ data-testid="modal-cancel-button"
5454+ ?disabled=${this.isPending}
5555+ @click=${() => dismiss(false)}
5656+ >
5757+ Cancel
5858+ </button>
5959+ <button
6060+ class="modal-dialog-button confirm-button primary-button"
6161+ data-testid="modal-confirm-button"
6262+ ?disabled=${this.isPending}
6363+ @click=${runConfirm}
6464+ >
6565+ ${this.isPending
6666+ ? html`<span
6767+ class="loading-spinner"
6868+ data-testid="loading-spinner"
6969+ ></span>`
7070+ : preview.requireApproval
7171+ ? "Send request"
7272+ : "Join"}
7373+ </button>
7474+ </div>
7575+ </div>
7676+ `;
7777+ }
7878+}
7979+580export class GroupChatLinkService {
681 constructor(dataLayer) {
782 this.dataLayer = dataLayer;
···27102 return;
28103 }
29104 if (actionType === "join" || actionType === "request") {
3030- this._openJoinDialog(preview);
105105+ JoinChatModal.open({
106106+ preview,
107107+ onSubmit: () => this._submit(preview),
108108+ });
31109 return;
32110 }
33111 if (actionType === "requested") {
3434- showInfoModal({
112112+ alertModal("The group owner will review your request.", {
35113 title: "Request pending",
3636- message: "The group owner will review your request.",
37114 });
38115 }
3939- }
4040-4141- _openJoinDialog(preview) {
4242- const name = preview.name ?? "";
4343- showActionModal({
4444- title: preview.requireApproval ? "Request to join" : "Join group chat",
4545- message: preview.requireApproval
4646- ? html`Send a request to join <strong>${name}</strong>. The group owner
4747- will review your request before you can see messages.`
4848- : html`You're about to join <strong>${name}</strong>.`,
4949- confirmButtonText: preview.requireApproval ? "Send request" : "Join",
5050- onConfirm: () => this._submit(preview),
5151- });
52116 }
5311754118 async _submit(preview) {
+3-3
src/js/listInteractionHandler.js
···11import { hapticsImpactMedium } from "/js/haptics.js";
22import { showToast } from "/js/toasts.js";
33-import { confirm } from "/js/modals.js";
33+import { confirmModal } from "/js/modals/confirm.modal.js";
4455export class ListInteractionHandler {
66 constructor(dataLayer) {
···2929 }
30303131 async handleMuteList(list) {
3232- const confirmed = await confirm(
3232+ const confirmed = await confirmModal(
3333 "The users on this list will be muted for you. Their posts won't appear in your feeds. Muting is private.",
3434 {
3535 title: "Mute these accounts?",
···5858 }
59596060 async handleBlockList(list) {
6161- const confirmed = await confirm(
6161+ const confirmed = await confirmModal(
6262 "The users on this list will be blocked. They won't be able to interact with you, and you won't see their content. Blocking is public.",
6363 {
6464 title: "Block these accounts?",
···11import { PluginBridge } from "/js/plugins/pluginBridge.js";
22-import { showPluginModal, hidePluginModal } from "/js/modals.js";
22+import {
33+ showPluginModal,
44+ hidePluginModal,
55+ showPluginInstallPermissionsModal,
66+ showPluginUpdatePermissionsModal,
77+} from "/js/plugins/pluginModal.js";
38import { showPluginToast, hidePluginToast, showToast } from "/js/toasts.js";
49import { PluginRenderer } from "/js/plugins/pluginRendering.js";
510import {
···1621 diffPermissions,
1722 isEmptyPermissions,
1823} from "/js/plugins/pluginPermissions.js";
1919-import {
2020- showPluginInstallPermissionsModal,
2121- showPluginUpdatePermissionsModal,
2222-} from "/js/modals.js";
2324import { compareVersions, isDev, sortBy } from "/js/utils.js";
2425import { Signal, SignalMap, ReactiveStore } from "/js/signals.js";
2526import { EventEmitter } from "/js/eventEmitter.js";
+4-4
src/js/postInteractionHandler.js
···11import { hapticsImpactMedium } from "/js/haptics.js";
22import { showToast } from "/js/toasts.js";
33-import { confirm } from "/js/modals.js";
33+import { confirmModal } from "/js/modals/confirm.modal.js";
44import { trashCanIconTemplate } from "/js/templates/icons/trashCanIcon.template.js";
5566export class PostInteractionHandler {
···73737474 async handleDeletePost(post) {
7575 if (
7676- !(await confirm(
7676+ !(await confirmModal(
7777 "If you remove this post, you won't be able to recover it.",
7878 {
7979 title: "Delete this post?",
···116116117117 async handleHidePost(post) {
118118 if (
119119- !(await confirm("This post will be hidden from feeds and threads.", {
119119+ !(await confirmModal("This post will be hidden from feeds and threads.", {
120120 title: "Hide this post?",
121121 confirmButtonText: "Hide",
122122 }))
···154154155155 async handleBlockAuthor(profile, doBlock) {
156156 if (doBlock) {
157157- const confirmed = await confirm(
157157+ const confirmed = await confirmModal(
158158 "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.",
159159 {
160160 title: "Block Account?",
+2-2
src/js/profileInteractionHandler.js
···11import { hapticsImpactMedium } from "/js/haptics.js";
22import { showToast } from "/js/toasts.js";
33-import { confirm } from "/js/modals.js";
33+import { confirmModal } from "/js/modals/confirm.modal.js";
44import { getDisplayName } from "/js/dataHelpers.js";
55import "/js/components/post-notifications-dialog.js";
66···54545555 async handleBlock(profile, doBlock) {
5656 if (doBlock) {
5757- const confirmed = await confirm(
5757+ const confirmed = await confirmModal(
5858 "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.",
5959 {
6060 title: "Block Account?",
···11import { html } from "/js/lib/lit-html.js";
22import { isAutomatedAccount } from "/js/dataHelpers.js";
33import { automatedAccountIconTemplate } from "/js/templates/icons/automatedAccountIcon.template.js";
44-import { showInfoModal } from "/js/modals.js";
44+import { alertModal } from "/js/modals/alert.modal.js";
5566export function automatedAccountBadgeTemplate({ profile }) {
77 if (!isAutomatedAccount(profile)) return "";
···1212 @click=${(e) => {
1313 e.preventDefault();
1414 e.stopPropagation();
1515- showInfoModal({
1515+ alertModal("This account has been marked as automated by its owner.", {
1616 title: "Automated account",
1717- message: "This account has been marked as automated by its owner.",
1817 confirmButtonText: "Okay",
1918 });
2019 }}
···2323} from "/js/navigation.js";
2424import "/js/components/animated-sidebar.js";
2525import "/js/components/plugin-icon.js";
2626-import { showInfoModal } from "/js/modals.js";
2626+import { alertModal } from "/js/modals/alert.modal.js";
27272828function pluginSidebarItemTemplate({ entry }) {
2929 return html`
···4545}
46464747function showAboutModal() {
4848- showInfoModal({
4949- title: "About Impro",
5050- message: html`<div>
4848+ alertModal(
4949+ html`<div>
5150 Impro is an <strong>alternative Bluesky client</strong> built from the
5251 ground up to be extensible and customizable. You can find more information
5352 about the project, including the full source code, at our
···5554 >GitHub repository</a
5655 >.
5756 </div>`,
5858- confirmButtonText: "Got it!",
5959- });
5757+ { title: "About Impro", confirmButtonText: "Got it!" },
5858+ );
6059}
61606261function sidebarNavTemplate({
+94-5
src/js/templates/whoCanReplyBadge.template.js
···11import { html } from "/js/lib/lit-html.js";
22import { getThreadgateAllowSettings } from "/js/dataHelpers.js";
33-import { showWhoCanReplyModal } from "/js/modals.js";
33+import { linkToProfile } from "/js/navigation.js";
44+import { Modal } from "/js/modals/modal.js";
45import { usersIconTemplate } from "/js/templates/icons/usersIcon.template.js";
56import { globeIconTemplate } from "/js/templates/icons/globeIcon.template.js";
6788+function ruleTemplate({ rule, author }) {
99+ if (rule.type === "mention") {
1010+ return html`mentioned users`;
1111+ }
1212+ if (rule.type === "followers") {
1313+ return html`users following
1414+ <a href=${linkToProfile(author)}>@${author.handle}</a>`;
1515+ }
1616+ if (rule.type === "following") {
1717+ return html`users followed by
1818+ <a href=${linkToProfile(author)}>@${author.handle}</a>`;
1919+ }
2020+ if (rule.type === "list") {
2121+ if (rule.list) {
2222+ return html`${rule.list.name} members`;
2323+ }
2424+ return html`list members`;
2525+ }
2626+ return html`unknown`;
2727+}
2828+2929+function threadgateRuleTemplate({ post }) {
3030+ const settings = getThreadgateAllowSettings(post);
3131+ if (!Array.isArray(settings)) {
3232+ if (settings.type === "everybody") {
3333+ return html`Everybody can reply to this post.`;
3434+ }
3535+ if (settings.type === "nobody") {
3636+ return html`Replies to this post are disabled.`;
3737+ }
3838+ }
3939+ if (Array.isArray(settings)) {
4040+ if (settings.some((rule) => rule.type === "unknown")) {
4141+ return html`This post has an unknown type of threadgate on it. Your app
4242+ may be out of date.`;
4343+ }
4444+ const author = post.author;
4545+ const parts = [];
4646+ settings.forEach((rule, i) => {
4747+ if (i > 0) {
4848+ if (i === settings.length - 1) {
4949+ parts.push(html`, and `);
5050+ } else {
5151+ parts.push(html`, `);
5252+ }
5353+ }
5454+ parts.push(ruleTemplate({ rule, author }));
5555+ });
5656+ return html`Only ${parts} can reply.`;
5757+ }
5858+ return null;
5959+}
6060+6161+export class WhoCanReplyModal extends Modal {
6262+ get className() {
6363+ return "bottom-sheet text-modal";
6464+ }
6565+6666+ get attributes() {
6767+ return { "data-testid": "who-can-reply-modal" };
6868+ }
6969+7070+ render({ dismiss, props: { post } }) {
7171+ const embeddingDisabled = !!post?.viewer?.embeddingDisabled;
7272+ return html`
7373+ <div class="modal-dialog-content">
7474+ <h2 class="modal-dialog-title" data-testid="modal-title">
7575+ Who can interact with this post?
7676+ </h2>
7777+ <div class="modal-dialog-message who-can-reply-body">
7878+ <span>${threadgateRuleTemplate({ post })}</span>
7979+ ${embeddingDisabled
8080+ ? html`<span>No one but the author can quote this post.</span>`
8181+ : ""}
8282+ </div>
8383+ <div class="modal-dialog-buttons">
8484+ <button
8585+ class="modal-dialog-button primary-button"
8686+ data-testid="modal-primary-button"
8787+ @click=${() => dismiss()}
8888+ >
8989+ OK
9090+ </button>
9191+ </div>
9292+ </div>
9393+ `;
9494+ }
9595+}
9696+797export function whoCanReplyBadgeTemplate({ post }) {
898 const settings = getThreadgateAllowSettings(post);
99- const embeddingDisabled = !!post?.viewer?.embeddingDisabled;
1099 const isEverybody = !Array.isArray(settings) && settings.type === "everybody";
11100 let label;
12101 let icon;
···25114 type="button"
26115 class="who-can-reply-badge"
27116 data-testid="who-can-reply-badge"
2828- @click=${(e) => {
2929- e.stopPropagation();
3030- showWhoCanReplyModal({ post });
117117+ @click=${(event) => {
118118+ event.stopPropagation();
119119+ WhoCanReplyModal.open({ post });
31120 }}
32121 >
33122 ${icon} ${label}
+10-7
src/js/views/settings.view.js
···1515import { linkToLogin } from "/js/navigation.js";
1616import "/js/components/context-menu.js";
1717import "/js/components/context-menu-item.js";
1818-import { confirm } from "/js/modals.js";
1818+import { confirmModal } from "/js/modals/confirm.modal.js";
1919import { showToast } from "/js/toasts.js";
2020import { Signal } from "/js/signals.js";
2121import { userIconTemplate } from "/js/templates/icons/userIcon.template.js";
···291291 });
292292 },
293293 onRemove: async (account) => {
294294- const ok = await confirm(
294294+ const ok = await confirmModal(
295295 `Remove @${account.handle} from this device?`,
296296 {
297297 title: "Remove account?",
···333333 data-testid="settings-sign-out"
334334 @click=${async () => {
335335 if (
336336- !(await confirm("Are you sure you want to sign out?", {
337337- title: "Sign out?",
338338- confirmButtonStyle: "danger",
339339- confirmButtonText: "Sign out",
340340- }))
336336+ !(await confirmModal(
337337+ "Are you sure you want to sign out?",
338338+ {
339339+ title: "Sign out?",
340340+ confirmButtonStyle: "danger",
341341+ confirmButtonText: "Sign out",
342342+ },
343343+ ))
341344 ) {
342345 return;
343346 }