diff --git a/mail/components/accountcreation/content/widgets/account-hub-footer.mjs b/mail/components/accountcreation/content/widgets/account-hub-footer.mjs
index 7f7bc6bd2b..24c15074b1 100644
--- a/mail/components/accountcreation/content/widgets/account-hub-footer.mjs
+++ b/mail/components/accountcreation/content/widgets/account-hub-footer.mjs
@@ -50,15 +50,14 @@ class AccountHubFooter extends HTMLElement {
}
toggleForwardDisabled(value) {
- this.querySelector("#forward").disabled = value;
+ this.querySelector("#forward").disabled = value || this.disabled;
}
canCustom(value) {
const customAction = this.querySelector("#custom");
customAction.hidden = !value;
- customAction.disabled = !value;
+ customAction.disabled = !value || this.disabled;
if (value) {
- customAction.disabled = false;
customAction.addEventListener("click", this);
document.l10n.setAttributes(customAction, value);
}
@@ -86,6 +85,19 @@ class AccountHubFooter extends HTMLElement {
relNotesLink.href = relNotesURL;
relNotesLink.closest("li[hidden]").hidden = false;
}
+
+ get disabled() {
+ return this.querySelector("#back").disabled;
+ }
+
+ set disabled(val) {
+ this.toggleForwardDisabled(val);
+ this.querySelector("#back").disabled = val;
+ const customAction = this.querySelector("#custom");
+ if (!customAction.hidden) {
+ customAction.disabled = val;
+ }
+ }
}
customElements.define("account-hub-footer", AccountHubFooter);
diff --git a/mail/components/accountcreation/content/widgets/email-auto-form.mjs b/mail/components/accountcreation/content/widgets/email-auto-form.mjs
index 8af5c345b2..2b0d2c0baf 100644
--- a/mail/components/accountcreation/content/widgets/email-auto-form.mjs
+++ b/mail/components/accountcreation/content/widgets/email-auto-form.mjs
@@ -116,6 +116,12 @@ class EmailAutoForm extends AccountHubStep {
captureState() {
return this.#currentConfig;
}
+
+ set disabled(val) {
+ for (const input of this.querySelectorAll("input")) {
+ input.disabled = val;
+ }
+ }
}
customElements.define("email-auto-form", EmailAutoForm);
diff --git a/mail/components/accountcreation/content/widgets/email-manual-outgoing-form.mjs b/mail/components/accountcreation/content/widgets/email-manual-outgoing-form.mjs
index 3e151b3ca3..4fba6f9ec7 100644
--- a/mail/components/accountcreation/content/widgets/email-manual-outgoing-form.mjs
+++ b/mail/components/accountcreation/content/widgets/email-manual-outgoing-form.mjs
@@ -341,6 +341,16 @@ class EmailOutgoingForm extends AccountHubStep {
this.#adjustOAuth2Visibility(config);
}
+
+ set disabled(val) {
+ this.#outgoingPort.disabled = val;
+ this.#outgoingUsername.disabled =
+ val || this.#outgoingAuthenticationMethod == 1;
+ this.#outgoingHostname.disabled = val;
+ this.#outgoingConnectionSecurity.disabled = val;
+ this.#outgoingAuthenticationMethod.disabled = val;
+ this.querySelector("#advancedConfigurationOutgoing").disabled = val;
+ }
}
customElements.define("email-manual-outgoing-form", EmailOutgoingForm);
diff --git a/mail/components/accountcreation/templates/accountHubTemplate.inc.xhtml b/mail/components/accountcreation/templates/accountHubTemplate.inc.xhtml
index 3116d0c64f..e36d9d38cb 100644
--- a/mail/components/accountcreation/templates/accountHubTemplate.inc.xhtml
+++ b/mail/components/accountcreation/templates/accountHubTemplate.inc.xhtml
@@ -105,4 +105,10 @@
+
+
diff --git a/mail/components/accountcreation/views/email.mjs b/mail/components/accountcreation/views/email.mjs
index fb99b18042..9b23513039 100644
--- a/mail/components/accountcreation/views/email.mjs
+++ b/mail/components/accountcreation/views/email.mjs
@@ -289,6 +289,7 @@ class AccountHubEmail extends HTMLElement {
* @param {string} subview - Subview for which the UI is being inititialized.
*/
async #initUI(subview) {
+ this.#stopLoading();
this.#hideSubviews();
this.#clearNotifications();
this.#currentState = subview;
@@ -352,6 +353,48 @@ class AccountHubEmail extends HTMLElement {
}
}
+ #loadingTimeout = null;
+
+ /**
+ * Show a loading notification and disable all inputs (except closing the
+ * dialog). If the load takes too long, a spinner is overlaid.
+ *
+ * TODO: should be able to cancel some loads, if they're abortable.
+ *
+ * @param {string} loadingFluentId
+ */
+ #startLoading(loadingFluentId) {
+ this.#states[this.#currentState].subview.showNotification({
+ fluentTitleId: loadingFluentId,
+ type: "info",
+ });
+ this.classList.add("busy");
+ this.#states[this.#currentState].subview.disabled = true;
+ this.#emailFooter.disabled = true;
+ this.#loadingTimeout = setTimeout(() => {
+ this.classList.add("spinner");
+ this.#loadingTimeout = null;
+ }, 3000);
+ }
+
+ /**
+ * Stop loading, clearing the notification, restoring form controls and hiding
+ * the spinner if it was visible.
+ */
+ #stopLoading() {
+ if (!this.classList.contains("busy")) {
+ return;
+ }
+ this.#clearNotifications();
+ this.#states[this.#currentState].subview.disabled = false;
+ this.#emailFooter.disabled = false;
+ this.classList.remove("busy", "spinner");
+ if (this.#loadingTimeout) {
+ clearTimeout(this.#loadingTimeout);
+ this.#loadingTimeout = null;
+ }
+ }
+
/**
* Handle the events from the subviews.
*
@@ -393,7 +436,11 @@ class AccountHubEmail extends HTMLElement {
this.#states[this.#currentState].subview.setState(config);
} catch (error) {
this.#handleAbortable();
- stateDetails.subview.showErrorNotification(error.title, error.text);
+ stateDetails.subview.showNotification({
+ title: error.title || error.message,
+ description: error.text,
+ type: "error",
+ });
}
break;
case "custom-footer-action":
@@ -487,6 +534,7 @@ class AccountHubEmail extends HTMLElement {
async #handleForwardAction(currentState, stateData) {
switch (currentState) {
case "autoConfigSubview":
+ this.#startLoading("account-hub-lookup-email-configuration-title");
try {
this.#emailFooter.canBack(true);
this.#email = stateData.email;
@@ -501,25 +549,32 @@ class AccountHubEmail extends HTMLElement {
this.#currentConfig = this.#fillAccountConfig(
this.#getEmptyAccountConfig()
);
+ this.#stopLoading();
await this.#initUI("incomingConfigSubview");
this.#states[this.#currentState].previousStep =
"autoConfigSubview";
- } else {
- this.#hasCancelled = false;
+ this.#states[this.#currentState].subview.showNotification({
+ fluentTitleId: "account-hub-find-settings-failed",
+ type: "warning",
+ });
break;
}
- } else {
- this.#currentConfig = this.#fillAccountConfig(config);
- await this.#initUI(this.#states[this.#currentState].nextStep);
- this.#states.incomingConfigSubview.previousStep =
- "emailConfigFoundSubview";
- this.#states[this.#currentState].subview.showNotification({
- fluentTitleId: "account-hub-config-success",
- type: "success",
- });
+ this.#hasCancelled = false;
+ this.#stopLoading();
+ break;
}
+ this.#currentConfig = this.#fillAccountConfig(config);
+ this.#stopLoading();
+ await this.#initUI(this.#states[this.#currentState].nextStep);
+ this.#states.incomingConfigSubview.previousStep =
+ "emailConfigFoundSubview";
+ this.#states[this.#currentState].subview.showNotification({
+ fluentTitleId: "account-hub-config-success",
+ type: "success",
+ });
} catch (error) {
this.#emailFooter.canBack(false);
+ this.#stopLoading();
if (!(error instanceof UserCancelledException)) {
// TODO: Throw proper error here;
throw error;
@@ -581,11 +636,11 @@ class AccountHubEmail extends HTMLElement {
case "incomingConfigSubview":
break;
case "outgoingConfigSubview":
+ this.#startLoading("account-hub-adding-account-subheader");
stateData = this.#states[this.#currentState].subview.captureState();
stateData.incoming =
this.#states.incomingConfigSubview.subview.captureState().config.incoming;
stateData = this.#fillAccountConfig(stateData);
-
try {
const config = await this.#guessConfig(
this.#email.split("@")[1],
@@ -593,17 +648,32 @@ class AccountHubEmail extends HTMLElement {
);
if (config.isComplete()) {
- // TODO: Show success message here.
+ this.#stopLoading();
+ this.#states[this.#currentState].subview.showNotification({
+ fluentTitleId: "account-hub-config-test-scucess",
+ type: "success",
+ });
this.#emailFooter.toggleForwardDisabled(false);
} else {
+ this.#stopLoading();
// The config is not complete, go back to the incoming view and
// show an error.
this.#initUI(this.#states[this.#currentState].previousStep);
// TODO: Show error message here.
+ this.#states[this.#currentState].subview.showNotification({
+ fluentTitleId: "account-hub-find-settings-failed",
+ type: "error",
+ });
}
} catch (error) {
+ this.#stopLoading();
this.#initUI(this.#states[this.#currentState].previousStep);
// TODO: Show error message here.
+ this.#states[this.#currentState].subview.showNotification({
+ fluentTitleId: "account-hub-find-settings-failed",
+ error,
+ type: "error",
+ });
}
break;
case "emailAddedSubview":
@@ -705,9 +775,9 @@ class AccountHubEmail extends HTMLElement {
gAccountSetupLogger.warn(`guessConfig failed: ${e}`);
reject(e);
- this.showNotification({
- title: "account-hub-find-settings-failed",
- e,
+ this.#states[this.#currentState].subview.showNotification({
+ fluentTitleId: "account-hub-find-settings-failed",
+ error: e,
type: "error",
});
this.abortable = null;
@@ -939,6 +1009,7 @@ class AccountHubEmail extends HTMLElement {
return false;
}
+ this.#stopLoading();
this.#currentState = "autoConfigSubview";
this.#currentConfig = {};
this.#hideSubviews();
diff --git a/mail/locales/en-US/messenger/accountcreation/accountHub.ftl b/mail/locales/en-US/messenger/accountcreation/accountHub.ftl
index 7bf292071b..fdcf61e2af 100644
--- a/mail/locales/en-US/messenger/accountcreation/accountHub.ftl
+++ b/mail/locales/en-US/messenger/accountcreation/accountHub.ftl
@@ -173,3 +173,5 @@ account-hub-password-info = Your credentials will only be stored locally on your
account-hub-sync-success = Thunderbird found some connected services
account-hub-email-added-success = Email account connected successfully
+
+account-hub-config-test-scucess = Configuration settings valid
diff --git a/mail/themes/shared/mail/accountHub.css b/mail/themes/shared/mail/accountHub.css
index 1b5306bdbc..f5303b90ee 100644
--- a/mail/themes/shared/mail/accountHub.css
+++ b/mail/themes/shared/mail/accountHub.css
@@ -35,6 +35,8 @@ dialog {
--hub-account-footer-link-color: var(--color-primary-default);
--hub-border-color: light-dark(var(--color-primary-soft), var(--color-primary-default));
--hub-divider-color: var(--color-neutral-base);
+ --hub-loader-background: var(--color-neutral-base);
+ --hub-loader-color: var(--color-primary-default);
--hub-box-shadow: 0 2px 4px rgba(58, 57, 68, 0.3);
--hub-input-height: 33px;
@@ -64,21 +66,20 @@ dialog {
.account-hub-dialog {
display: grid;
width: 800px;
- height: 600px;
+ min-height: 600px;
overflow: initial;
padding: 0;
box-shadow: none;
&::after {
+ --hub-blur-radius: 15px;
content: '';
position: absolute;
- top: 17%;
- left: 50%;
- width: 765px;
- height: 500px;
- transform: translateX(-50%);
+ inset-block-end: -2px;
+ inset-inline: calc(2 * var(--hub-blur-radius) + 5px);
+ min-height: calc(3 * var(--hub-blur-radius));
background: linear-gradient(to right, rgba(159, 244, 240, 1), rgba(76, 177, 249, 1), rgba(168, 85, 247, 1));
- filter: blur(15px);
+ filter: blur(var(--hub-blur-radius));
z-index: -1;
}
@@ -118,6 +119,106 @@ dialog {
@media (prefers-color-scheme: dark) {
background-image: url("chrome://messenger/skin/images/accounthub-bg-dark.webp");
}
+
+ &.busy {
+ cursor: wait;
+ }
+
+ #loadingOverlay {
+ display: none;
+ opacity: 0;
+ }
+
+ &.spinner #loadingOverlay {
+ display: grid;
+ position: absolute;
+ inset: 0;
+ place-items: center;
+ background-color: var(--hub-loader-background);
+ opacity: 0.8;
+ z-index: 2;
+ border-radius: inherit;
+
+ @media (prefers-reduced-motion: no-preference) {
+ animation: 0.5s linear 0s hub-reveal-loader;
+ }
+ }
+}
+
+@keyframes hub-reveal-loader {
+ 0% {
+ display: none;
+ opacity: 0;
+ }
+
+ 1% {
+ display: grid;
+ opacity: 0;
+ }
+
+ 100% {
+ opacity: 0.8;
+ }
+}
+
+@keyframes hub-loader-loading {
+ from {
+ rotate: 0deg;
+ }
+ to {
+ rotate: 360deg;
+ }
+}
+
+.loader-outside {
+ --hub-loader-width: 8px;
+ --hub-loader-size: 64px;
+ --hub-trail-offset: 360deg;
+
+ position: relative;
+ height: var(--hub-loader-size);
+ aspect-ratio: 1;
+ background-image: conic-gradient(
+ from 0deg,
+ transparent 0deg,
+ var(--hub-loader-color) 360deg,
+ transparent var(--hub-trail-offset)
+ );
+ border-radius: 50%;
+ pointer-events: none;
+
+
+ @media (prefers-reduced-motion: no-preference) {
+ animation: 1.1s cubic-bezier(0.61, 0.12, 0, 0.99) 0s infinite hub-loader-loading;
+ }
+
+ &::before {
+ content: '';
+ position: absolute;
+ inset: var(--hub-loader-width);
+ height: calc(var(--hub-loader-size) - 2 * var(--hub-loader-width));
+ aspect-ratio: 1;
+ background: var(--hub-loader-background);
+ border-radius: 50%;
+ }
+
+ .loader-inside {
+ position: absolute;
+ inset: 0;
+ aspect-ratio: 1;
+ border-radius: 50%;
+
+ &::before {
+ content: '';
+ position: absolute;
+ height: var(--hub-loader-width);
+ aspect-ratio: 1;
+ border-radius: 50%;
+ background: var(--hub-loader-color);
+ inset-block-start: 0;
+ inset-inline: calc(50% - var(--hub-loader-width) / 2);
+ }
+ }
}
/* Typography */