mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 02:09:05 +02:00 
			
		
		
		
	Bug 1965529 - Add saving custom image functionality to profiles avatar picker. r=profiles-reviewers,fluent-reviewers,bolsson,jhirsch
				
					
				
			Differential Revision: https://phabricator.services.mozilla.com/D250926
This commit is contained in:
		
							parent
							
								
									6fa0204b44
								
							
						
					
					
						commit
						e87c1712fb
					
				
					 20 changed files with 642 additions and 78 deletions
				
			
		| 
						 | 
				
			
			@ -104,11 +104,9 @@ var gProfiles = {
 | 
			
		|||
      "label",
 | 
			
		||||
      SelectableProfileService.currentProfile.name
 | 
			
		||||
    );
 | 
			
		||||
    let avatar = SelectableProfileService.currentProfile.avatar;
 | 
			
		||||
    profilesButton.setAttribute(
 | 
			
		||||
      "image",
 | 
			
		||||
      `chrome://browser/content/profiles/assets/16_${avatar}.svg`
 | 
			
		||||
    );
 | 
			
		||||
    let avatarURL =
 | 
			
		||||
      await SelectableProfileService.currentProfile.getAvatarURL(16);
 | 
			
		||||
    profilesButton.setAttribute("image", `${avatarURL}`);
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
| 
						 | 
				
			
			@ -131,7 +129,7 @@ var gProfiles = {
 | 
			
		|||
      menuitem.setAttribute("label", profile.name);
 | 
			
		||||
      menuitem.style.setProperty("--menu-profiles-theme-bg", themeBg);
 | 
			
		||||
      menuitem.style.setProperty("--menu-profiles-theme-fg", themeFg);
 | 
			
		||||
      menuitem.style.listStyleImage = `url(chrome://browser/content/profiles/assets/48_${profile.avatar}.svg)`;
 | 
			
		||||
      menuitem.style.listStyleImage = `url(${await profile.getAvatarURL(48)})`;
 | 
			
		||||
      menuitem.classList.add("menuitem-iconic", "menuitem-iconic-profile");
 | 
			
		||||
 | 
			
		||||
      if (profile.id === currentProfile.id) {
 | 
			
		||||
| 
						 | 
				
			
			@ -325,8 +323,7 @@ var gProfiles = {
 | 
			
		|||
        themeFg
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      let avatar = currentProfile.avatar;
 | 
			
		||||
      profileIconEl.style.listStyleImage = `url("chrome://browser/content/profiles/assets/80_${avatar}.svg")`;
 | 
			
		||||
      profileIconEl.style.listStyleImage = `url(${await currentProfile.getAvatarURL(80)})`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let subtitle = PanelMultiView.getViewNode(document, "profiles-subtitle");
 | 
			
		||||
| 
						 | 
				
			
			@ -348,10 +345,7 @@ var gProfiles = {
 | 
			
		|||
      let { themeFg, themeBg } = profile.theme;
 | 
			
		||||
      button.style.setProperty("--appmenu-profiles-theme-bg", themeBg);
 | 
			
		||||
      button.style.setProperty("--appmenu-profiles-theme-fg", themeFg);
 | 
			
		||||
      button.setAttribute(
 | 
			
		||||
        "image",
 | 
			
		||||
        `chrome://browser/content/profiles/assets/16_${profile.avatar}.svg`
 | 
			
		||||
      );
 | 
			
		||||
      button.setAttribute("image", `${await profile.getAvatarURL(16)}`);
 | 
			
		||||
 | 
			
		||||
      profilesList.appendChild(button);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -183,8 +183,8 @@ export class ProfilesParent extends JSWindowActorParent {
 | 
			
		|||
    let profiles = await SelectableProfileService.getAllProfiles();
 | 
			
		||||
    let themes = await this.getSafeForContentThemes();
 | 
			
		||||
    return {
 | 
			
		||||
      currentProfile: currentProfile.toObject(),
 | 
			
		||||
      profiles: profiles.map(p => p.toObject()),
 | 
			
		||||
      currentProfile: await currentProfile.toContentSafeObject(),
 | 
			
		||||
      profiles: await Promise.all(profiles.map(p => p.toContentSafeObject())),
 | 
			
		||||
      themes,
 | 
			
		||||
      isInAutomation: Cu.isInAutomation,
 | 
			
		||||
    };
 | 
			
		||||
| 
						 | 
				
			
			@ -282,7 +282,8 @@ export class ProfilesParent extends JSWindowActorParent {
 | 
			
		|||
        // Make sure SelectableProfileService is initialized
 | 
			
		||||
        await SelectableProfileService.init();
 | 
			
		||||
        Glean.profilesDelete.displayed.record();
 | 
			
		||||
        let profileObj = SelectableProfileService.currentProfile.toObject();
 | 
			
		||||
        let profileObj =
 | 
			
		||||
          await SelectableProfileService.currentProfile.toContentSafeObject();
 | 
			
		||||
        let windowCount = lazy.EveryWindow.readyWindows.length;
 | 
			
		||||
        let tabCount = lazy.EveryWindow.readyWindows
 | 
			
		||||
          .flatMap(win => win.gBrowser.openTabs.length)
 | 
			
		||||
| 
						 | 
				
			
			@ -326,14 +327,20 @@ export class ProfilesParent extends JSWindowActorParent {
 | 
			
		|||
        };
 | 
			
		||||
      }
 | 
			
		||||
      case "Profiles:UpdateProfileAvatar": {
 | 
			
		||||
        let avatar = message.data.avatar;
 | 
			
		||||
        SelectableProfileService.currentProfile.avatar = avatar;
 | 
			
		||||
        let { avatarOrFile } = message.data;
 | 
			
		||||
        await SelectableProfileService.currentProfile.setAvatar(avatarOrFile);
 | 
			
		||||
        let value = SelectableProfileService.currentProfile.isCustomAvatar
 | 
			
		||||
          ? "custom"
 | 
			
		||||
          : avatarOrFile;
 | 
			
		||||
 | 
			
		||||
        if (source === "about:editprofile") {
 | 
			
		||||
          Glean.profilesExisting.avatar.record({ value: avatar });
 | 
			
		||||
          Glean.profilesExisting.avatar.record({ value });
 | 
			
		||||
        } else if (source === "about:newprofile") {
 | 
			
		||||
          Glean.profilesNew.avatar.record({ value: avatar });
 | 
			
		||||
          Glean.profilesNew.avatar.record({ value });
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
        let profileObj =
 | 
			
		||||
          await SelectableProfileService.currentProfile.toContentSafeObject();
 | 
			
		||||
        return profileObj;
 | 
			
		||||
      }
 | 
			
		||||
      case "Profiles:UpdateProfileTheme": {
 | 
			
		||||
        let themeId = message.data;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,42 @@
 | 
			
		|||
import { ProfilesDatastoreService } from "moz-src:///toolkit/profile/ProfilesDatastoreService.sys.mjs";
 | 
			
		||||
import { SelectableProfileService } from "resource:///modules/profiles/SelectableProfileService.sys.mjs";
 | 
			
		||||
 | 
			
		||||
const STANDARD_AVATARS = new Set([
 | 
			
		||||
  "barbell",
 | 
			
		||||
  "bike",
 | 
			
		||||
  "book",
 | 
			
		||||
  "briefcase",
 | 
			
		||||
  "canvas",
 | 
			
		||||
  "craft",
 | 
			
		||||
  "default-favicon",
 | 
			
		||||
  "diamond",
 | 
			
		||||
  "flower",
 | 
			
		||||
  "folder",
 | 
			
		||||
  "hammer",
 | 
			
		||||
  "heart",
 | 
			
		||||
  "heart-rate",
 | 
			
		||||
  "history",
 | 
			
		||||
  "leaf",
 | 
			
		||||
  "lightbulb",
 | 
			
		||||
  "makeup",
 | 
			
		||||
  "message",
 | 
			
		||||
  "musical-note",
 | 
			
		||||
  "palette",
 | 
			
		||||
  "paw-print",
 | 
			
		||||
  "plane",
 | 
			
		||||
  "present",
 | 
			
		||||
  "shopping",
 | 
			
		||||
  "soccer",
 | 
			
		||||
  "sparkle-single",
 | 
			
		||||
  "star",
 | 
			
		||||
  "video-game-controller",
 | 
			
		||||
]);
 | 
			
		||||
const STANDARD_AVATAR_SIZES = [16, 20, 24, 48, 60, 80];
 | 
			
		||||
 | 
			
		||||
function standardAvatarURL(avatar, size = "80") {
 | 
			
		||||
  return `chrome://browser/content/profiles/assets/${size}_${avatar}.svg`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The selectable profile
 | 
			
		||||
 */
 | 
			
		||||
| 
						 | 
				
			
			@ -19,10 +55,15 @@ export class SelectableProfile {
 | 
			
		|||
  // The user-editable name
 | 
			
		||||
  #name;
 | 
			
		||||
 | 
			
		||||
  // Name of the user's chosen avatar, which corresponds to a list of built-in
 | 
			
		||||
  // SVG avatars.
 | 
			
		||||
  // Name of the user's chosen avatar, which corresponds to a list of standard
 | 
			
		||||
  // SVG avatars. Or if the avatar is a custom image, the filename of the image
 | 
			
		||||
  // stored in the avatars directory.
 | 
			
		||||
  #avatar;
 | 
			
		||||
 | 
			
		||||
  // lastAvatarURL is saved when URL.createObjectURL is invoked so we can
 | 
			
		||||
  // revoke the url at a later time.
 | 
			
		||||
  #lastAvatarURL;
 | 
			
		||||
 | 
			
		||||
  // Cached theme properties, used to allow displaying a SelectableProfile
 | 
			
		||||
  // without loading the AddonManager to get theme info.
 | 
			
		||||
  #themeId;
 | 
			
		||||
| 
						 | 
				
			
			@ -122,16 +163,116 @@ export class SelectableProfile {
 | 
			
		|||
    return this.#avatar;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the path of the current avatar.
 | 
			
		||||
   * If the avatar is standard, the return value will be of the form
 | 
			
		||||
   * 'chrome://browser/content/profiles/assets/{avatar}.svg'.
 | 
			
		||||
   * If the avatar is custom, the return value will be the path to the file on
 | 
			
		||||
   * disk.
 | 
			
		||||
   *
 | 
			
		||||
   * @returns {string} Path to the current avatar.
 | 
			
		||||
   */
 | 
			
		||||
  getAvatarPath(size) {
 | 
			
		||||
    if (!this.hasCustomAvatar) {
 | 
			
		||||
      return standardAvatarURL(this.avatar, size);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return PathUtils.join(
 | 
			
		||||
      ProfilesDatastoreService.constructor.PROFILE_GROUPS_DIR,
 | 
			
		||||
      "avatars",
 | 
			
		||||
      this.avatar
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the URL of the current avatar.
 | 
			
		||||
   * If the avatar is standard, the return value will be of the form
 | 
			
		||||
   * 'chrome://browser/content/profiles/assets/${size}_${avatar}.svg'.
 | 
			
		||||
   * If the avatar is custom, the return value will be a blob URL.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {string|number} size optional Must be one of the sizes in
 | 
			
		||||
   * STANDARD_AVATAR_SIZES. Will be converted to a string.
 | 
			
		||||
   *
 | 
			
		||||
   * @returns {Promise<string>} Resolves to the URL of the current avatar
 | 
			
		||||
   */
 | 
			
		||||
  async getAvatarURL(size) {
 | 
			
		||||
    if (!this.hasCustomAvatar) {
 | 
			
		||||
      return standardAvatarURL(this.avatar, size);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.#lastAvatarURL) {
 | 
			
		||||
      URL.revokeObjectURL(this.#lastAvatarURL);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const fileExists = await IOUtils.exists(this.getAvatarPath());
 | 
			
		||||
    if (!fileExists) {
 | 
			
		||||
      throw new Error("Custom avatar file doesn't exist.");
 | 
			
		||||
    }
 | 
			
		||||
    const file = await File.createFromFileName(this.getAvatarPath());
 | 
			
		||||
    this.#lastAvatarURL = URL.createObjectURL(file);
 | 
			
		||||
 | 
			
		||||
    return this.#lastAvatarURL;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the avatar file. This is only used for custom avatars to generate an
 | 
			
		||||
   * object url. Standard avatars should use getAvatarURL or getAvatarPath.
 | 
			
		||||
   *
 | 
			
		||||
   * @returns {Promise<File>} Resolves to a file of the avatar
 | 
			
		||||
   */
 | 
			
		||||
  async getAvatarFile() {
 | 
			
		||||
    if (!this.hasCustomAvatar) {
 | 
			
		||||
      throw new Error(
 | 
			
		||||
        "Profile does not have custom avatar. Custom avatar file doesn't exist."
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return File.createFromFileName(this.getAvatarPath());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get hasCustomAvatar() {
 | 
			
		||||
    return !STANDARD_AVATARS.has(this.avatar);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Update the avatar, then trigger saving the profile, which will notify()
 | 
			
		||||
   * other running instances.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {string} aAvatar Name of the avatar
 | 
			
		||||
   * @param {string|File} aAvatarOrFile Name of the avatar or File os custom avatar
 | 
			
		||||
   */
 | 
			
		||||
  set avatar(aAvatar) {
 | 
			
		||||
    this.#avatar = aAvatar;
 | 
			
		||||
  async setAvatar(aAvatarOrFile) {
 | 
			
		||||
    if (aAvatarOrFile === this.avatar) {
 | 
			
		||||
      // The avatar is the same so do nothing. See the comment in
 | 
			
		||||
      // SelectableProfileService.maybeSetupDataStore for resetting the avatar
 | 
			
		||||
      // to draw the avatar icon in the dock.
 | 
			
		||||
    } else if (STANDARD_AVATARS.has(aAvatarOrFile)) {
 | 
			
		||||
      this.#avatar = aAvatarOrFile;
 | 
			
		||||
    } else {
 | 
			
		||||
      await this.#uploadCustomAvatar(aAvatarOrFile);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.saveUpdatesToDB();
 | 
			
		||||
    await this.saveUpdatesToDB();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async #uploadCustomAvatar(file) {
 | 
			
		||||
    const avatarsDir = PathUtils.join(
 | 
			
		||||
      ProfilesDatastoreService.constructor.PROFILE_GROUPS_DIR,
 | 
			
		||||
      "avatars"
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Create avatars directory if it does not exist
 | 
			
		||||
    await IOUtils.makeDirectory(avatarsDir, { ignoreExisting: true });
 | 
			
		||||
 | 
			
		||||
    let uuid = Services.uuid.generateUUID().toString().slice(1, -1);
 | 
			
		||||
 | 
			
		||||
    const filePath = PathUtils.join(avatarsDir, uuid);
 | 
			
		||||
 | 
			
		||||
    const arrayBuffer = await file.arrayBuffer();
 | 
			
		||||
    const uint8Array = new Uint8Array(arrayBuffer);
 | 
			
		||||
 | 
			
		||||
    await IOUtils.write(filePath, uint8Array, { tmpPath: `${filePath}.tmp` });
 | 
			
		||||
 | 
			
		||||
    this.#avatar = uuid;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
| 
						 | 
				
			
			@ -155,7 +296,7 @@ export class SelectableProfile {
 | 
			
		|||
        return "star-avatar-alt";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return "";
 | 
			
		||||
    return "custom-avatar-alt";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Note, theme properties are set and returned as a group.
 | 
			
		||||
| 
						 | 
				
			
			@ -205,16 +346,66 @@ export class SelectableProfile {
 | 
			
		|||
    SelectableProfileService.updateProfile(this);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toObject() {
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns on object with only fields needed for the database.
 | 
			
		||||
   *
 | 
			
		||||
   * @returns {object} An object with only fields need for the database
 | 
			
		||||
   */
 | 
			
		||||
  async toDbObject() {
 | 
			
		||||
    let profileObj = {
 | 
			
		||||
      id: this.id,
 | 
			
		||||
      path: this.#path,
 | 
			
		||||
      name: this.name,
 | 
			
		||||
      avatar: this.avatar,
 | 
			
		||||
      ...this.theme,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return profileObj;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns an object representation of the profile.
 | 
			
		||||
   * Note: No custom avatar URLs are included because URL.createObjectURL needs
 | 
			
		||||
   * to be invoked in the content process for the avatar to be visible.
 | 
			
		||||
   *
 | 
			
		||||
   * @returns {object} An object representation of the profile
 | 
			
		||||
   */
 | 
			
		||||
  async toContentSafeObject() {
 | 
			
		||||
    let profileObj = {
 | 
			
		||||
      id: this.id,
 | 
			
		||||
      path: this.#path,
 | 
			
		||||
      name: this.name,
 | 
			
		||||
      avatar: this.avatar,
 | 
			
		||||
      avatarL10nId: this.avatarL10nId,
 | 
			
		||||
      hasCustomAvatar: this.hasCustomAvatar,
 | 
			
		||||
      ...this.theme,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (this.hasCustomAvatar) {
 | 
			
		||||
      let path = this.getAvatarPath();
 | 
			
		||||
      let file = await this.getAvatarFile();
 | 
			
		||||
 | 
			
		||||
      profileObj.avatarPaths = Object.fromEntries(
 | 
			
		||||
        STANDARD_AVATAR_SIZES.map(s => [`path${s}`, path])
 | 
			
		||||
      );
 | 
			
		||||
      profileObj.avatarFiles = Object.fromEntries(
 | 
			
		||||
        STANDARD_AVATAR_SIZES.map(s => [`file${s}`, file])
 | 
			
		||||
      );
 | 
			
		||||
      profileObj.avatarURLs = {};
 | 
			
		||||
    } else {
 | 
			
		||||
      profileObj.avatarPaths = Object.fromEntries(
 | 
			
		||||
        STANDARD_AVATAR_SIZES.map(s => [`path${s}`, this.getAvatarPath(s)])
 | 
			
		||||
      );
 | 
			
		||||
      profileObj.avatarURLs = Object.fromEntries(
 | 
			
		||||
        await Promise.all(
 | 
			
		||||
          STANDARD_AVATAR_SIZES.map(async s => [
 | 
			
		||||
            `url${s}`,
 | 
			
		||||
            await this.getAvatarURL(s),
 | 
			
		||||
          ])
 | 
			
		||||
        )
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return profileObj;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -55,7 +55,7 @@ const COMMAND_LINE_ACTIVATE = "profiles-activate";
 | 
			
		|||
 | 
			
		||||
const gSupportsBadging = "nsIMacDockSupport" in Ci || "nsIWinTaskbar" in Ci;
 | 
			
		||||
 | 
			
		||||
function loadImage(url) {
 | 
			
		||||
function loadBuiltInAvatarImage(uri, channel) {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    let imageTools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools);
 | 
			
		||||
    let imageContainer;
 | 
			
		||||
| 
						 | 
				
			
			@ -67,15 +67,8 @@ function loadImage(url) {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    imageTools.decodeImageFromChannelAsync(
 | 
			
		||||
      url,
 | 
			
		||||
      Services.io.newChannelFromURI(
 | 
			
		||||
        url,
 | 
			
		||||
        null,
 | 
			
		||||
        Services.scriptSecurityManager.getSystemPrincipal(),
 | 
			
		||||
        null, // aTriggeringPrincipal
 | 
			
		||||
        Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
 | 
			
		||||
        Ci.nsIContentPolicy.TYPE_IMAGE
 | 
			
		||||
      ),
 | 
			
		||||
      uri,
 | 
			
		||||
      channel,
 | 
			
		||||
      (image, status) => {
 | 
			
		||||
        if (!Components.isSuccessCode(status)) {
 | 
			
		||||
          reject(new Components.Exception("Image loading failed", status));
 | 
			
		||||
| 
						 | 
				
			
			@ -88,6 +81,66 @@ function loadImage(url) {
 | 
			
		|||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getCustomAvatarImageType(blob, channel) {
 | 
			
		||||
  let octets = await new Promise((resolve, reject) => {
 | 
			
		||||
    let reader = new FileReader();
 | 
			
		||||
    reader.addEventListener("load", () => {
 | 
			
		||||
      resolve(Array.from(reader.result).map(c => c.charCodeAt(0)));
 | 
			
		||||
    });
 | 
			
		||||
    reader.addEventListener("error", reject);
 | 
			
		||||
    reader.readAsBinaryString(blob);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  let sniffer = Cc["@mozilla.org/image/loader;1"].createInstance(
 | 
			
		||||
    Ci.nsIContentSniffer
 | 
			
		||||
  );
 | 
			
		||||
  let type = sniffer.getMIMETypeFromContent(channel, octets, octets.length);
 | 
			
		||||
 | 
			
		||||
  return type;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function loadCustomAvatarImage(profile, channel) {
 | 
			
		||||
  let blob = await profile.getAvatarFile();
 | 
			
		||||
 | 
			
		||||
  const imageTools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools);
 | 
			
		||||
  let type = await getCustomAvatarImageType(blob, channel);
 | 
			
		||||
 | 
			
		||||
  let buffer = await blob.arrayBuffer();
 | 
			
		||||
  const image = imageTools.decodeImageFromArrayBuffer(buffer, type);
 | 
			
		||||
 | 
			
		||||
  return image;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function loadImage(profile) {
 | 
			
		||||
  let uri;
 | 
			
		||||
 | 
			
		||||
  if (SelectableProfileService.currentProfile.hasCustomAvatar) {
 | 
			
		||||
    const file = await IOUtils.getFile(
 | 
			
		||||
      SelectableProfileService.currentProfile.getAvatarPath(48)
 | 
			
		||||
    );
 | 
			
		||||
    uri = Services.io.newFileURI(file);
 | 
			
		||||
  } else {
 | 
			
		||||
    uri = Services.io.newURI(
 | 
			
		||||
      SelectableProfileService.currentProfile.getAvatarPath(48)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const channel = Services.io.newChannelFromURI(
 | 
			
		||||
    uri,
 | 
			
		||||
    null,
 | 
			
		||||
    Services.scriptSecurityManager.getSystemPrincipal(),
 | 
			
		||||
    null,
 | 
			
		||||
    Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
 | 
			
		||||
    Ci.nsIContentPolicy.TYPE_IMAGE
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (profile.isCustomAvatar) {
 | 
			
		||||
    return loadCustomAvatarImage(profile, channel);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return loadBuiltInAvatarImage(uri, channel);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The service that manages selectable profiles
 | 
			
		||||
 */
 | 
			
		||||
| 
						 | 
				
			
			@ -589,13 +642,7 @@ class SelectableProfileServiceClass extends EventEmitter {
 | 
			
		|||
 | 
			
		||||
      if (count > 1 && !this.#badge) {
 | 
			
		||||
        this.#badge = {
 | 
			
		||||
          image: await loadImage(
 | 
			
		||||
            Services.io.newURI(
 | 
			
		||||
              `chrome://browser/content/profiles/assets/48_${
 | 
			
		||||
                this.#currentProfile.avatar
 | 
			
		||||
              }.svg`
 | 
			
		||||
            )
 | 
			
		||||
          ),
 | 
			
		||||
          image: await loadImage(this.#currentProfile),
 | 
			
		||||
          iconPaintContext: this.#currentProfile.iconPaintContext,
 | 
			
		||||
          description: this.#currentProfile.name,
 | 
			
		||||
        };
 | 
			
		||||
| 
						 | 
				
			
			@ -611,6 +658,12 @@ class SelectableProfileServiceClass extends EventEmitter {
 | 
			
		|||
              .getOverlayIconController(win.docShell);
 | 
			
		||||
            TASKBAR_ICON_CONTROLLERS.set(win, iconController);
 | 
			
		||||
 | 
			
		||||
            if (this.#currentProfile.isCustomAvatar) {
 | 
			
		||||
              iconController.setOverlayIcon(
 | 
			
		||||
                this.#badge.image,
 | 
			
		||||
                this.#badge.description
 | 
			
		||||
              );
 | 
			
		||||
            } else {
 | 
			
		||||
              iconController.setOverlayIcon(
 | 
			
		||||
                this.#badge.image,
 | 
			
		||||
                this.#badge.description,
 | 
			
		||||
| 
						 | 
				
			
			@ -618,6 +671,7 @@ class SelectableProfileServiceClass extends EventEmitter {
 | 
			
		|||
              );
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } else if (count <= 1 && this.#badge) {
 | 
			
		||||
        this.#badge = null;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1116,7 +1170,7 @@ class SelectableProfileServiceClass extends EventEmitter {
 | 
			
		|||
        lazy.setTimeout(() => {
 | 
			
		||||
          // To avoid displeasing the linter, assign to a temporary variable.
 | 
			
		||||
          let avatar = SelectableProfileService.currentProfile.avatar;
 | 
			
		||||
          SelectableProfileService.currentProfile.avatar = avatar;
 | 
			
		||||
          SelectableProfileService.currentProfile.setAvatar(avatar);
 | 
			
		||||
        }, 1000);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -1239,8 +1293,7 @@ class SelectableProfileServiceClass extends EventEmitter {
 | 
			
		|||
   * @param {SelectableProfile} aSelectableProfile The SelectableProfile to be updated
 | 
			
		||||
   */
 | 
			
		||||
  async updateProfile(aSelectableProfile) {
 | 
			
		||||
    let profileObj = aSelectableProfile.toObject();
 | 
			
		||||
    delete profileObj.avatarL10nId;
 | 
			
		||||
    let profileObj = await aSelectableProfile.toDbObject();
 | 
			
		||||
 | 
			
		||||
    await this.#connection.execute(
 | 
			
		||||
      `UPDATE Profiles
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -39,6 +39,12 @@ export class DeleteProfileCard extends MozLitElement {
 | 
			
		|||
 | 
			
		||||
    this.data = await RPMSendQuery("Profiles:GetDeleteProfileContent");
 | 
			
		||||
 | 
			
		||||
    if (this.data.profile.hasCustomAvatar) {
 | 
			
		||||
      const objURL = URL.createObjectURL(this.data.profile.avatarFiles.file16);
 | 
			
		||||
      this.data.profile.avatarURLs.url16 = objURL;
 | 
			
		||||
      this.data.profile.avatarURLs.url80 = objURL;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let titleEl = document.querySelector("title");
 | 
			
		||||
    titleEl.setAttribute(
 | 
			
		||||
      "data-l10n-args",
 | 
			
		||||
| 
						 | 
				
			
			@ -64,7 +70,7 @@ export class DeleteProfileCard extends MozLitElement {
 | 
			
		|||
 | 
			
		||||
  setFavicon() {
 | 
			
		||||
    let favicon = document.getElementById("favicon");
 | 
			
		||||
    favicon.href = `chrome://browser/content/profiles/assets/16_${this.data.profile.avatar}.svg`;
 | 
			
		||||
    favicon.href = this.data.profile.avatarURLs.url16;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  cancelDelete() {
 | 
			
		||||
| 
						 | 
				
			
			@ -95,8 +101,7 @@ export class DeleteProfileCard extends MozLitElement {
 | 
			
		|||
            width="80"
 | 
			
		||||
            height="80"
 | 
			
		||||
            data-l10n-id=${this.data.profile.avatarL10nId}
 | 
			
		||||
            src="chrome://browser/content/profiles/assets/80_${this.data.profile
 | 
			
		||||
              .avatar}.svg"
 | 
			
		||||
            src=${this.data.profile.avatarURLs.url80}
 | 
			
		||||
          />
 | 
			
		||||
          <div id="profile-content">
 | 
			
		||||
            <div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@
 | 
			
		|||
    <meta charset="utf-8" />
 | 
			
		||||
    <meta
 | 
			
		||||
      http-equiv="Content-Security-Policy"
 | 
			
		||||
      content="default-src resource: chrome:; object-src 'none'; img-src chrome:;"
 | 
			
		||||
      content="default-src resource: chrome:; object-src 'none'; img-src blob: chrome:;"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <link id="favicon" rel="icon" type="image/svg+xml" />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -125,7 +125,11 @@ export class EditProfileCard extends MozLitElement {
 | 
			
		|||
 | 
			
		||||
    window.addEventListener("beforeunload", this);
 | 
			
		||||
    window.addEventListener("pagehide", this);
 | 
			
		||||
 | 
			
		||||
    if (RPMGetBoolPref(UPDATED_AVATAR_SELECTOR_PREF, false)) {
 | 
			
		||||
      document.addEventListener("click", this);
 | 
			
		||||
      document.addEventListener("Profiles:CustomAvatarUpload", this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.init().then(() => (this.initialized = true));
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -146,9 +150,27 @@ export class EditProfileCard extends MozLitElement {
 | 
			
		|||
    this.profiles = profiles;
 | 
			
		||||
    this.themes = themes;
 | 
			
		||||
 | 
			
		||||
    if (this.profile.hasCustomAvatar) {
 | 
			
		||||
      this.createAvatarURL();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.setFavicon();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createAvatarURL() {
 | 
			
		||||
    if (this.profile.avatarURLs.url16) {
 | 
			
		||||
      URL.revokeObjectURL(this.profile.avatarURLs.url16);
 | 
			
		||||
      delete this.profile.avatarURLs.url16;
 | 
			
		||||
      delete this.profile.avatarURLs.url80;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.profile.avatarFiles?.file16) {
 | 
			
		||||
      const objURL = URL.createObjectURL(this.profile.avatarFiles.file16);
 | 
			
		||||
      this.profile.avatarURLs.url16 = objURL;
 | 
			
		||||
      this.profile.avatarURLs.url80 = objURL;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getUpdateComplete() {
 | 
			
		||||
    const result = await super.getUpdateComplete();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -163,7 +185,7 @@ export class EditProfileCard extends MozLitElement {
 | 
			
		|||
 | 
			
		||||
  setFavicon() {
 | 
			
		||||
    let favicon = document.getElementById("favicon");
 | 
			
		||||
    favicon.href = `chrome://browser/content/profiles/assets/16_${this.profile.avatar}.svg`;
 | 
			
		||||
    favicon.href = this.profile.avatarURLs.url16;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getAvatarL10nId(value) {
 | 
			
		||||
| 
						 | 
				
			
			@ -208,6 +230,11 @@ export class EditProfileCard extends MozLitElement {
 | 
			
		|||
        this.avatarSelector.hidden = true;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case "Profiles:CustomAvatarUpload": {
 | 
			
		||||
        let { file } = event.detail;
 | 
			
		||||
        this.updateAvatar(file);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -254,8 +281,16 @@ export class EditProfileCard extends MozLitElement {
 | 
			
		|||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.profile.avatar = newAvatar;
 | 
			
		||||
    RPMSendAsyncMessage("Profiles:UpdateProfileAvatar", this.profile);
 | 
			
		||||
    let updatedProfile = await RPMSendQuery("Profiles:UpdateProfileAvatar", {
 | 
			
		||||
      avatarOrFile: newAvatar,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.profile = updatedProfile;
 | 
			
		||||
 | 
			
		||||
    if (this.profile.hasCustomAvatar) {
 | 
			
		||||
      this.createAvatarURL();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.requestUpdate();
 | 
			
		||||
    this.setFavicon();
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -413,8 +448,7 @@ export class EditProfileCard extends MozLitElement {
 | 
			
		|||
        <img
 | 
			
		||||
          id="header-avatar"
 | 
			
		||||
          data-l10n-id=${this.profile.avatarL10nId}
 | 
			
		||||
          src="chrome://browser/content/profiles/assets/80_${this.profile
 | 
			
		||||
            .avatar}.svg"
 | 
			
		||||
          src=${this.profile.avatarURLs.url80}
 | 
			
		||||
        />
 | 
			
		||||
        <a
 | 
			
		||||
          id="profile-avatar-selector-link"
 | 
			
		||||
| 
						 | 
				
			
			@ -433,8 +467,7 @@ export class EditProfileCard extends MozLitElement {
 | 
			
		|||
    return html`<img
 | 
			
		||||
      id="header-avatar"
 | 
			
		||||
      data-l10n-id=${this.profile.avatarL10nId}
 | 
			
		||||
      src="chrome://browser/content/profiles/assets/20_${this.profile
 | 
			
		||||
        .avatar}.svg"
 | 
			
		||||
      src=${this.profile.avatarURLs.url80}
 | 
			
		||||
    />`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@
 | 
			
		|||
    <meta charset="utf-8" />
 | 
			
		||||
    <meta
 | 
			
		||||
      http-equiv="Content-Security-Policy"
 | 
			
		||||
      content="default-src chrome:; object-src 'none'; img-src chrome:;"
 | 
			
		||||
      content="default-src chrome:; object-src 'none'; img-src blob: chrome:;"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <link id="favicon" rel="icon" type="image/svg+xml" />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,6 +31,10 @@ export class NewProfileCard extends EditProfileCard {
 | 
			
		|||
    this.profiles = profiles;
 | 
			
		||||
    this.themes = themes;
 | 
			
		||||
 | 
			
		||||
    if (this.profile.hasCustomAvatar) {
 | 
			
		||||
      super.createAvatarURL();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
      this.setInitialInput(),
 | 
			
		||||
      this.setRandomTheme(isInAutomation),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@
 | 
			
		|||
    <meta charset="utf-8" />
 | 
			
		||||
    <meta
 | 
			
		||||
      http-equiv="Content-Security-Policy"
 | 
			
		||||
      content="default-src chrome:; object-src 'none'; img-src chrome:;"
 | 
			
		||||
      content="default-src chrome:; object-src 'none'; img-src blob: chrome:;"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <link id="favicon" rel="icon" type="image/svg+xml" />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,3 +29,51 @@
 | 
			
		|||
  --button-icon-fill: transparent;
 | 
			
		||||
  --button-icon-stroke: currentColor;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.custom-avatar-area {
 | 
			
		||||
  width: 230px;
 | 
			
		||||
  height: 194px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#upload-text {
 | 
			
		||||
  color: var(--link-color);
 | 
			
		||||
  text-decoration: underline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#drag-text {
 | 
			
		||||
  font-size: var(--font-size-small);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#custom-avatar-image {
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
  max-height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.custom-avatar-actions {
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#custom-image {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#file-messages {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: var(--space-small);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,8 +12,20 @@ import { html } from "chrome://global/content/vendor/lit.all.mjs";
 | 
			
		|||
export class ProfileAvatarSelector extends MozLitElement {
 | 
			
		||||
  static properties = {
 | 
			
		||||
    value: { type: String },
 | 
			
		||||
    state: { type: String },
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static queries = {
 | 
			
		||||
    input: "#custom-image",
 | 
			
		||||
    saveButton: "#save-button",
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    super();
 | 
			
		||||
 | 
			
		||||
    this.state = "custom";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getAvatarL10nId(value) {
 | 
			
		||||
    switch (value) {
 | 
			
		||||
      case "book":
 | 
			
		||||
| 
						 | 
				
			
			@ -33,7 +45,7 @@ export class ProfileAvatarSelector extends MozLitElement {
 | 
			
		|||
    return "";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  iconTabContent() {
 | 
			
		||||
  iconTabContentTemplate() {
 | 
			
		||||
    let avatars = [
 | 
			
		||||
      "star",
 | 
			
		||||
      "flower",
 | 
			
		||||
| 
						 | 
				
			
			@ -90,12 +102,104 @@ export class ProfileAvatarSelector extends MozLitElement {
 | 
			
		|||
    >`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  customTabUploadFileContentTemplate() {
 | 
			
		||||
    return html`<div class="custom-avatar-area">
 | 
			
		||||
      <input
 | 
			
		||||
        @change=${this.handleFileUpload}
 | 
			
		||||
        id="custom-image"
 | 
			
		||||
        type="file"
 | 
			
		||||
        accept="image/*"
 | 
			
		||||
        label="Upload a file"
 | 
			
		||||
      />
 | 
			
		||||
      <div id="file-messages">
 | 
			
		||||
        <img src="chrome://browser/skin/open.svg" />
 | 
			
		||||
        <span
 | 
			
		||||
          id="upload-text"
 | 
			
		||||
          data-l10n-id="avatar-selector-upload-file"
 | 
			
		||||
        ></span>
 | 
			
		||||
        <span id="drag-text" data-l10n-id="avatar-selector-drag-file"></span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleCancelClick(event) {
 | 
			
		||||
    event.stopImmediatePropagation();
 | 
			
		||||
 | 
			
		||||
    this.state = "custom";
 | 
			
		||||
    if (this.blobURL) {
 | 
			
		||||
      URL.revokeObjectURL(this.blobURL);
 | 
			
		||||
    }
 | 
			
		||||
    this.file = null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async handleSaveClick(event) {
 | 
			
		||||
    event.stopImmediatePropagation();
 | 
			
		||||
 | 
			
		||||
    document.dispatchEvent(
 | 
			
		||||
      new CustomEvent("Profiles:CustomAvatarUpload", {
 | 
			
		||||
        detail: { file: this.file },
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (this.blobURL) {
 | 
			
		||||
      URL.revokeObjectURL(this.blobURL);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.state = "custom";
 | 
			
		||||
    this.hidden = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  customTabViewImageTemplate() {
 | 
			
		||||
    return html`<div class="custom-avatar-area">
 | 
			
		||||
        <img id="custom-avatar-image" src=${this.blobURL} />
 | 
			
		||||
      </div>
 | 
			
		||||
      <moz-button-group class="custom-avatar-actions"
 | 
			
		||||
        ><moz-button
 | 
			
		||||
          @click=${this.handleCancelClick}
 | 
			
		||||
          data-l10n-id="avatar-selector-cancel-button"
 | 
			
		||||
        ></moz-button
 | 
			
		||||
        ><moz-button
 | 
			
		||||
          type="primary"
 | 
			
		||||
          id="save-button"
 | 
			
		||||
          @click=${this.handleSaveClick}
 | 
			
		||||
          data-l10n-id="avatar-selector-save-button"
 | 
			
		||||
        ></moz-button
 | 
			
		||||
      ></moz-button-group>`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleFileUpload(event) {
 | 
			
		||||
    const [file] = event.target.files;
 | 
			
		||||
    this.file = file;
 | 
			
		||||
 | 
			
		||||
    if (this.blobURL) {
 | 
			
		||||
      URL.revokeObjectURL(this.blobURL);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.blobURL = URL.createObjectURL(file);
 | 
			
		||||
    this.state = "crop";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  contentTemplate() {
 | 
			
		||||
    switch (this.state) {
 | 
			
		||||
      case "icon": {
 | 
			
		||||
        return this.iconTabContentTemplate();
 | 
			
		||||
      }
 | 
			
		||||
      case "custom": {
 | 
			
		||||
        return this.customTabUploadFileContentTemplate();
 | 
			
		||||
      }
 | 
			
		||||
      case "crop": {
 | 
			
		||||
        return this.customTabViewImageTemplate();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    return html`<link
 | 
			
		||||
        rel="stylesheet"
 | 
			
		||||
        href="chrome://browser/content/profiles/profile-avatar-selector.css"
 | 
			
		||||
      />
 | 
			
		||||
      <moz-card>
 | 
			
		||||
      <moz-card id="avatar-selector">
 | 
			
		||||
        <div class="button-group">
 | 
			
		||||
          <moz-button
 | 
			
		||||
            type="primary"
 | 
			
		||||
| 
						 | 
				
			
			@ -103,7 +207,7 @@ export class ProfileAvatarSelector extends MozLitElement {
 | 
			
		|||
          ></moz-button
 | 
			
		||||
          ><moz-button data-l10n-id="avatar-selector-custom-tab"></moz-button>
 | 
			
		||||
        </div>
 | 
			
		||||
        ${this.iconTabContent()}
 | 
			
		||||
        ${this.contentTemplate()}
 | 
			
		||||
      </moz-card>`;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,8 +50,8 @@ export class ProfileCard extends MozLitElement {
 | 
			
		|||
    this.backgroundImage.style.stroke = themeFg;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setAvatarImage() {
 | 
			
		||||
    this.avatarImage.style.backgroundImage = `url("chrome://browser/content/profiles/assets/80_${this.profile.avatar}.svg")`;
 | 
			
		||||
  async setAvatarImage() {
 | 
			
		||||
    this.avatarImage.style.backgroundImage = `url(${await this.profile.getAvatarURL(80)})`;
 | 
			
		||||
    let { themeFg, themeBg } = this.profile.theme;
 | 
			
		||||
    this.avatarImage.style.fill = themeBg;
 | 
			
		||||
    this.avatarImage.style.stroke = themeFg;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@
 | 
			
		|||
    <meta charset="utf-8" />
 | 
			
		||||
    <meta
 | 
			
		||||
      http-equiv="Content-Security-Policy"
 | 
			
		||||
      content="default-src resource: chrome:; object-src 'none'; img-src chrome:;"
 | 
			
		||||
      content="default-src resource: chrome:; object-src 'none'; img-src blob: chrome:;"
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
    <link rel="localization" href="branding/brand.ftl" />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -171,7 +171,7 @@ add_task(async function test_new_profile_avatar() {
 | 
			
		|||
  let expectedAvatar = "briefcase";
 | 
			
		||||
  // Before we load the new page, set the profile's avatar to something other
 | 
			
		||||
  // than the expected item.
 | 
			
		||||
  profile.avatar = "flower";
 | 
			
		||||
  await profile.setAvatar("flower");
 | 
			
		||||
  await SelectableProfileService.updateProfile(profile);
 | 
			
		||||
 | 
			
		||||
  await BrowserTestUtils.withNewTab(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,7 +31,6 @@ add_task(async function test_edit_profile_custom_avatar() {
 | 
			
		|||
    },
 | 
			
		||||
    async browser => {
 | 
			
		||||
      await SpecialPowers.spawn(browser, [], async () => {
 | 
			
		||||
        await new Promise(r => content.setTimeout(r, 4000));
 | 
			
		||||
        let editProfileCard =
 | 
			
		||||
          content.document.querySelector("edit-profile-card").wrappedJSObject;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -55,5 +54,119 @@ add_task(async function test_edit_profile_custom_avatar() {
 | 
			
		|||
      });
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  await SpecialPowers.popPrefEnv();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
add_task(async function test_edit_profile_custom_avatar_upload() {
 | 
			
		||||
  if (!AppConstants.MOZ_SELECTABLE_PROFILES) {
 | 
			
		||||
    // `mochitest-browser` suite `add_task` does not yet support
 | 
			
		||||
    // `properties.skip_if`.
 | 
			
		||||
    ok(true, "Skipping because !AppConstants.MOZ_SELECTABLE_PROFILES");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  const profile = await setup();
 | 
			
		||||
 | 
			
		||||
  await SpecialPowers.pushPrefEnv({
 | 
			
		||||
    set: [["browser.profiles.updated-avatar-selector", true]],
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const mockAvatarFilePath = await IOUtils.createUniqueFile(
 | 
			
		||||
    PathUtils.tempDir,
 | 
			
		||||
    "avatar.png"
 | 
			
		||||
  );
 | 
			
		||||
  const mockAvatarFile = Cc["@mozilla.org/file/local;1"].createInstance(
 | 
			
		||||
    Ci.nsIFile
 | 
			
		||||
  );
 | 
			
		||||
  mockAvatarFile.initWithPath(mockAvatarFilePath);
 | 
			
		||||
 | 
			
		||||
  const MockFilePicker = SpecialPowers.MockFilePicker;
 | 
			
		||||
  MockFilePicker.init(window.browsingContext);
 | 
			
		||||
  MockFilePicker.setFiles([mockAvatarFile]);
 | 
			
		||||
  MockFilePicker.returnValue = MockFilePicker.returnOK;
 | 
			
		||||
 | 
			
		||||
  let curProfile = await SelectableProfileService.getProfile(profile.id);
 | 
			
		||||
  Assert.ok(
 | 
			
		||||
    !curProfile.hasCustomAvatar,
 | 
			
		||||
    "Current profile does not have a custom avatar"
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  await BrowserTestUtils.withNewTab(
 | 
			
		||||
    {
 | 
			
		||||
      gBrowser,
 | 
			
		||||
      url: "about:editprofile",
 | 
			
		||||
    },
 | 
			
		||||
    async browser => {
 | 
			
		||||
      await SpecialPowers.spawn(browser, [], async () => {
 | 
			
		||||
        let editProfileCard =
 | 
			
		||||
          content.document.querySelector("edit-profile-card").wrappedJSObject;
 | 
			
		||||
 | 
			
		||||
        await ContentTaskUtils.waitForCondition(
 | 
			
		||||
          () => editProfileCard.initialized,
 | 
			
		||||
          "Waiting for edit-profile-card to be initialized"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        await editProfileCard.updateComplete;
 | 
			
		||||
 | 
			
		||||
        const avatarSelector = editProfileCard.avatarSelector;
 | 
			
		||||
 | 
			
		||||
        EventUtils.synthesizeMouseAtCenter(
 | 
			
		||||
          editProfileCard.avatarSelectorLink,
 | 
			
		||||
          {},
 | 
			
		||||
          content
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        Assert.ok(
 | 
			
		||||
          ContentTaskUtils.isVisible(avatarSelector),
 | 
			
		||||
          "Should be showing the profile avatar selector"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        avatarSelector.state = "custom";
 | 
			
		||||
        await avatarSelector.updateComplete;
 | 
			
		||||
 | 
			
		||||
        await ContentTaskUtils.waitForCondition(
 | 
			
		||||
          () => ContentTaskUtils.isVisible(avatarSelector.input),
 | 
			
		||||
          "Waiting for avatar selector input to be visible"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const inputReceived = new Promise(resolve =>
 | 
			
		||||
          avatarSelector.input.addEventListener(
 | 
			
		||||
            "input",
 | 
			
		||||
            event => {
 | 
			
		||||
              resolve(event.target.files[0].name);
 | 
			
		||||
            },
 | 
			
		||||
            { once: true }
 | 
			
		||||
          )
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        EventUtils.synthesizeMouseAtCenter(avatarSelector.input, {}, content);
 | 
			
		||||
 | 
			
		||||
        await inputReceived;
 | 
			
		||||
 | 
			
		||||
        await ContentTaskUtils.waitForCondition(
 | 
			
		||||
          () => ContentTaskUtils.isVisible(avatarSelector.saveButton),
 | 
			
		||||
          "Waiting for avatar selector save button to be visible"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        EventUtils.synthesizeMouseAtCenter(
 | 
			
		||||
          avatarSelector.saveButton,
 | 
			
		||||
          {},
 | 
			
		||||
          content
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Sometimes the async message takes a bit longer to arrive.
 | 
			
		||||
        await new Promise(resolve => content.setTimeout(resolve, 100));
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  curProfile = await SelectableProfileService.getProfile(profile.id);
 | 
			
		||||
  Assert.ok(
 | 
			
		||||
    curProfile.hasCustomAvatar,
 | 
			
		||||
    "Current profile has a custom avatar image"
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  MockFilePicker.cleanup();
 | 
			
		||||
 | 
			
		||||
  await SpecialPowers.popPrefEnv();
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -148,7 +148,7 @@ add_task(async function test_edit_profile_avatar() {
 | 
			
		|||
 | 
			
		||||
  // Before we load the edit page, set the profile's avatar to something other
 | 
			
		||||
  // than the 0th item.
 | 
			
		||||
  profile.avatar = "flower";
 | 
			
		||||
  await profile.setAvatar("flower");
 | 
			
		||||
  let expectedAvatar = "book";
 | 
			
		||||
 | 
			
		||||
  is(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,6 +47,12 @@ edit-profile-page-delete-button =
 | 
			
		|||
edit-profile-page-avatar-selector-opener-link = Edit
 | 
			
		||||
avatar-selector-icon-tab = Icon
 | 
			
		||||
avatar-selector-custom-tab = Custom
 | 
			
		||||
avatar-selector-cancel-button =
 | 
			
		||||
  .label = Cancel
 | 
			
		||||
avatar-selector-save-button =
 | 
			
		||||
  .label = Save
 | 
			
		||||
avatar-selector-upload-file = Upload a file
 | 
			
		||||
avatar-selector-drag-file = Or drag a file here
 | 
			
		||||
 | 
			
		||||
edit-profile-page-no-name = Name this profile to help you find it later. Rename it any time.
 | 
			
		||||
edit-profile-page-duplicate-name = Profile name already in use. Try a new name.
 | 
			
		||||
| 
						 | 
				
			
			@ -134,6 +140,8 @@ shopping-avatar-alt =
 | 
			
		|||
    .alt = Shopping cart
 | 
			
		||||
star-avatar-alt =
 | 
			
		||||
    .alt = Star
 | 
			
		||||
custom-avatar-alt =
 | 
			
		||||
    .alt = Custom avatar
 | 
			
		||||
 | 
			
		||||
## Labels for default avatar icons
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1319,15 +1319,19 @@ static nsLiteralCString sStyleSrcUnsafeInlineAllowList[] = {
 | 
			
		|||
static nsLiteralCString sImgSrcDataBlobAllowList[] = {
 | 
			
		||||
    "about:addons"_ns,
 | 
			
		||||
    "about:debugging"_ns,
 | 
			
		||||
    "about:deleteprofile"_ns,
 | 
			
		||||
    "about:devtools-toolbox"_ns,
 | 
			
		||||
    "about:editprofile"_ns,
 | 
			
		||||
    "about:firefoxview"_ns,
 | 
			
		||||
    "about:home"_ns,
 | 
			
		||||
    "about:inference"_ns,
 | 
			
		||||
    "about:logins"_ns,
 | 
			
		||||
    "about:newprofile"_ns,
 | 
			
		||||
    "about:newtab"_ns,
 | 
			
		||||
    "about:preferences"_ns,
 | 
			
		||||
    "about:privatebrowsing"_ns,
 | 
			
		||||
    "about:processes"_ns,
 | 
			
		||||
    "about:profilemanager"_ns,
 | 
			
		||||
    "about:protections"_ns,
 | 
			
		||||
    "about:reader"_ns,
 | 
			
		||||
    "about:sessionrestore"_ns,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -145,10 +145,10 @@ export let RemotePageAccessManager = {
 | 
			
		|||
      RPMSendQuery: [
 | 
			
		||||
        "Profiles:GetEditProfileContent",
 | 
			
		||||
        "Profiles:UpdateProfileTheme",
 | 
			
		||||
        "Profiles:UpdateProfileAvatar",
 | 
			
		||||
      ],
 | 
			
		||||
      RPMSendAsyncMessage: [
 | 
			
		||||
        "Profiles:UpdateProfileName",
 | 
			
		||||
        "Profiles:UpdateProfileAvatar",
 | 
			
		||||
        "Profiles:OpenDeletePage",
 | 
			
		||||
        "Profiles:CloseProfileTab",
 | 
			
		||||
        "Profiles:MoreThemes",
 | 
			
		||||
| 
						 | 
				
			
			@ -160,10 +160,10 @@ export let RemotePageAccessManager = {
 | 
			
		|||
      RPMSendQuery: [
 | 
			
		||||
        "Profiles:GetNewProfileContent",
 | 
			
		||||
        "Profiles:UpdateProfileTheme",
 | 
			
		||||
        "Profiles:UpdateProfileAvatar",
 | 
			
		||||
      ],
 | 
			
		||||
      RPMSendAsyncMessage: [
 | 
			
		||||
        "Profiles:UpdateProfileName",
 | 
			
		||||
        "Profiles:UpdateProfileAvatar",
 | 
			
		||||
        "Profiles:DeleteProfile",
 | 
			
		||||
        "Profiles:CloseProfileTab",
 | 
			
		||||
        "Profiles:MoreThemes",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue