WallpaperFeed.sys.mjs (12025B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 6 7 const lazy = {}; 8 ChromeUtils.defineESModuleGetters(lazy, { 9 BasePromiseWorker: "resource://gre/modules/PromiseWorker.sys.mjs", 10 RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", 11 Utils: "resource://services-settings/Utils.sys.mjs", 12 }); 13 14 import { 15 actionTypes as at, 16 actionCreators as ac, 17 } from "resource://newtab/common/Actions.mjs"; 18 19 const PREF_WALLPAPERS_ENABLED = 20 "browser.newtabpage.activity-stream.newtabWallpapers.enabled"; 21 22 const PREF_WALLPAPERS_HIGHLIGHT_SEEN_COUNTER = 23 "browser.newtabpage.activity-stream.newtabWallpapers.highlightSeenCounter"; 24 25 const WALLPAPER_REMOTE_SETTINGS_COLLECTION_V2 = "newtab-wallpapers-v2"; 26 27 const PREF_WALLPAPERS_CUSTOM_WALLPAPER_ENABLED = 28 "browser.newtabpage.activity-stream.newtabWallpapers.customWallpaper.enabled"; 29 30 const PREF_WALLPAPERS_CUSTOM_WALLPAPER_UUID = 31 "browser.newtabpage.activity-stream.newtabWallpapers.customWallpaper.uuid"; 32 33 const PREF_SELECTED_WALLPAPER = 34 "browser.newtabpage.activity-stream.newtabWallpapers.wallpaper"; 35 36 const RS_FALLBACK_BASE_URL = 37 "https://firefox-settings-attachments.cdn.mozilla.net/"; 38 39 export class WallpaperFeed { 40 #customBackgroundObjectURL = null; 41 42 // @backward-compat { version 148 } This newtab train-hop compatibility 43 // shim can be removed once Firefox 148 makes it to the release channel. 44 #usesProtocolHandler = 45 Services.vc.compare(AppConstants.MOZ_APP_VERSION, "148.0a1") >= 0; 46 47 constructor() { 48 this.loaded = false; 49 this.wallpaperClient = null; 50 this._onSync = this.onSync.bind(this); 51 } 52 53 // Constructs a moz-newtab-wallpaper:// URI for the given wallpaper UUID. 54 getWallpaperURL(uuid) { 55 return `moz-newtab-wallpaper://${uuid}`; 56 } 57 58 /** 59 * This thin wrapper around global.fetch makes it easier for us to write 60 * automated tests that simulate responses from this fetch. 61 */ 62 fetch(...args) { 63 return fetch(...args); 64 } 65 66 /** 67 * This thin wrapper around lazy.RemoteSettings makes it easier for us to write 68 * automated tests that simulate responses from this fetch. 69 */ 70 RemoteSettings(...args) { 71 return lazy.RemoteSettings(...args); 72 } 73 74 /** 75 * This thin wrapper around lazy.BasePromiseWorker makes it easier for us to write 76 * automated tests 77 */ 78 BasePromiseWorker(...args) { 79 return new lazy.BasePromiseWorker(...args); 80 } 81 82 async wallpaperSetup(isStartup = false) { 83 const wallpapersEnabled = Services.prefs.getBoolPref( 84 PREF_WALLPAPERS_ENABLED 85 ); 86 87 if (wallpapersEnabled) { 88 if (!this.wallpaperClient) { 89 // getting collection 90 this.wallpaperClient = this.RemoteSettings( 91 WALLPAPER_REMOTE_SETTINGS_COLLECTION_V2 92 ); 93 } 94 95 this.wallpaperClient.on("sync", this._onSync); 96 this.updateWallpapers(isStartup); 97 } 98 } 99 100 async wallpaperTeardown() { 101 if (this._onSync) { 102 this.wallpaperClient?.off("sync", this._onSync); 103 } 104 this.loaded = false; 105 this.wallpaperClient = null; 106 } 107 108 async onSync() { 109 this.wallpaperTeardown(); 110 await this.wallpaperSetup(false /* isStartup */); 111 } 112 113 async updateWallpapers(isStartup = false) { 114 // @backward-compat { version 148 } This newtab train-hop compatibility 115 // shim can be removed once Firefox 148 makes it to the release channel. 116 if (!this.#usesProtocolHandler) { 117 if (this.#customBackgroundObjectURL) { 118 URL.revokeObjectURL(this.#customBackgroundObjectURL); 119 this.#customBackgroundObjectURL = null; 120 } 121 } 122 123 let uuid = Services.prefs.getStringPref( 124 PREF_WALLPAPERS_CUSTOM_WALLPAPER_UUID, 125 "" 126 ); 127 128 const selectedWallpaper = Services.prefs.getStringPref( 129 PREF_SELECTED_WALLPAPER, 130 "" 131 ); 132 133 if (uuid && selectedWallpaper === "custom") { 134 // @backward-compat { version 148 } This newtab train-hop compatibility 135 // shim can be removed once Firefox 148 makes it to the release channel. 136 if (this.#usesProtocolHandler) { 137 const wallpaperURI = this.getWallpaperURL(uuid); 138 139 this.store.dispatch( 140 ac.BroadcastToContent({ 141 type: at.WALLPAPERS_CUSTOM_SET, 142 data: wallpaperURI, 143 }) 144 ); 145 } else { 146 const wallpaperDir = PathUtils.join(PathUtils.profileDir, "wallpaper"); 147 const filePath = PathUtils.join(wallpaperDir, uuid); 148 149 try { 150 let testFile = await IOUtils.getFile(filePath); 151 152 if (!testFile) { 153 throw new Error("File does not exist"); 154 } 155 156 let imageFile = await File.createFromNsIFile(testFile); 157 this.#customBackgroundObjectURL = URL.createObjectURL(imageFile); 158 159 this.store.dispatch( 160 ac.BroadcastToContent({ 161 type: at.WALLPAPERS_CUSTOM_SET, 162 data: this.#customBackgroundObjectURL, 163 }) 164 ); 165 } catch (error) { 166 console.warn(`Wallpaper file not found: ${error.message}`); 167 Services.prefs.clearUserPref(PREF_WALLPAPERS_CUSTOM_WALLPAPER_UUID); 168 return; 169 } 170 } 171 } else { 172 this.store.dispatch( 173 ac.BroadcastToContent({ 174 type: at.WALLPAPERS_CUSTOM_SET, 175 data: null, 176 }) 177 ); 178 } 179 180 // retrieving all records in collection 181 const records = await this.wallpaperClient.get(); 182 if (!records?.length) { 183 return; 184 } 185 186 const customWallpaperEnabled = Services.prefs.getBoolPref( 187 PREF_WALLPAPERS_CUSTOM_WALLPAPER_ENABLED 188 ); 189 190 let baseAttachmentURL = RS_FALLBACK_BASE_URL; 191 try { 192 baseAttachmentURL = await lazy.Utils.baseAttachmentsURL(); 193 } catch (error) { 194 console.error( 195 `Error fetching remote settings base url from CDN. Falling back to ${RS_FALLBACK_BASE_URL}`, 196 error 197 ); 198 } 199 200 const wallpapers = [ 201 ...records.map(record => { 202 return { 203 ...record, 204 ...(record.attachment 205 ? { 206 wallpaperUrl: `${baseAttachmentURL}${record.attachment.location}`, 207 } 208 : {}), 209 background_position: record.background_position || "center", 210 category: record.category || "", 211 order: record.order || 0, 212 }; 213 }), 214 ]; 215 216 const categories = [ 217 ...new Set( 218 wallpapers.map(wallpaper => wallpaper.category).filter(Boolean) 219 ), 220 ...(customWallpaperEnabled ? ["custom-wallpaper"] : []), // Conditionally add custom wallpaper input 221 ]; 222 223 this.store.dispatch( 224 ac.BroadcastToContent({ 225 type: at.WALLPAPERS_SET, 226 data: wallpapers, 227 meta: { 228 isStartup, 229 }, 230 }) 231 ); 232 233 this.store.dispatch( 234 ac.BroadcastToContent({ 235 type: at.WALLPAPERS_CATEGORY_SET, 236 data: categories, 237 meta: { 238 isStartup, 239 }, 240 }) 241 ); 242 } 243 244 initHighlightCounter() { 245 let counter = Services.prefs.getIntPref( 246 PREF_WALLPAPERS_HIGHLIGHT_SEEN_COUNTER 247 ); 248 249 this.store.dispatch( 250 ac.AlsoToPreloaded({ 251 type: at.WALLPAPERS_FEATURE_HIGHLIGHT_COUNTER_INCREMENT, 252 data: { 253 value: counter, 254 }, 255 }) 256 ); 257 } 258 259 wallpaperSeenEvent() { 260 let counter = Services.prefs.getIntPref( 261 PREF_WALLPAPERS_HIGHLIGHT_SEEN_COUNTER 262 ); 263 264 const newCount = counter + 1; 265 266 this.store.dispatch( 267 ac.OnlyToMain({ 268 type: at.SET_PREF, 269 data: { 270 name: "newtabWallpapers.highlightSeenCounter", 271 value: newCount, 272 }, 273 }) 274 ); 275 276 this.store.dispatch( 277 ac.AlsoToPreloaded({ 278 type: at.WALLPAPERS_FEATURE_HIGHLIGHT_COUNTER_INCREMENT, 279 data: { 280 value: newCount, 281 }, 282 }) 283 ); 284 } 285 286 async wallpaperUpload(file) { 287 try { 288 const customWallpaperThemeWorker = this.BasePromiseWorker( 289 "resource://newtab/lib/Wallpapers/WallpaperTheme.worker.mjs", 290 { type: "module" } 291 ); 292 const wallpaperTheme = await customWallpaperThemeWorker.post( 293 "calculateTheme", 294 [file] 295 ); 296 customWallpaperThemeWorker.terminate(); 297 const wallpaperDir = PathUtils.join(PathUtils.profileDir, "wallpaper"); 298 299 // create wallpaper directory if it does not exist 300 await IOUtils.makeDirectory(wallpaperDir, { ignoreExisting: true }); 301 302 let uuid = Services.uuid.generateUUID().toString().slice(1, -1); 303 Services.prefs.setStringPref(PREF_WALLPAPERS_CUSTOM_WALLPAPER_UUID, uuid); 304 305 const filePath = PathUtils.join(wallpaperDir, uuid); 306 307 // convert to Uint8Array for IOUtils 308 const arrayBuffer = await file.arrayBuffer(); 309 const uint8Array = new Uint8Array(arrayBuffer); 310 311 await IOUtils.write(filePath, uint8Array, { tmpPath: `${filePath}.tmp` }); 312 313 // @backward-compat { version 148 } This newtab train-hop compatibility 314 // shim can be removed once Firefox 148 makes it to the release channel. 315 if (this.#usesProtocolHandler) { 316 const wallpaperURI = this.getWallpaperURL(uuid); 317 318 this.store.dispatch( 319 ac.BroadcastToContent({ 320 type: at.WALLPAPERS_CUSTOM_SET, 321 data: wallpaperURI, 322 }) 323 ); 324 } else { 325 if (this.#customBackgroundObjectURL) { 326 URL.revokeObjectURL(this.#customBackgroundObjectURL); 327 this.#customBackgroundObjectURL = null; 328 } 329 330 this.#customBackgroundObjectURL = URL.createObjectURL(file); 331 332 this.store.dispatch( 333 ac.BroadcastToContent({ 334 type: at.WALLPAPERS_CUSTOM_SET, 335 data: this.#customBackgroundObjectURL, 336 }) 337 ); 338 } 339 340 this.store.dispatch( 341 ac.SetPref("newtabWallpapers.customWallpaper.theme", wallpaperTheme) 342 ); 343 344 return filePath; 345 } catch (error) { 346 console.error("Error saving wallpaper:", error); 347 return null; 348 } 349 } 350 351 async removeCustomWallpaper() { 352 try { 353 let uuid = Services.prefs.getStringPref( 354 PREF_WALLPAPERS_CUSTOM_WALLPAPER_UUID, 355 "" 356 ); 357 358 if (!uuid) { 359 return; 360 } 361 362 const wallpaperDir = PathUtils.join(PathUtils.profileDir, "wallpaper"); 363 const filePath = PathUtils.join(wallpaperDir, uuid); 364 365 await IOUtils.remove(filePath, { ignoreAbsent: true }); 366 367 Services.prefs.clearUserPref(PREF_WALLPAPERS_CUSTOM_WALLPAPER_UUID); 368 369 this.store.dispatch( 370 ac.BroadcastToContent({ 371 type: at.WALLPAPERS_CUSTOM_SET, 372 data: null, 373 }) 374 ); 375 } catch (error) { 376 console.error("Failed to remove custom wallpaper:", error); 377 } 378 } 379 380 async onAction(action) { 381 switch (action.type) { 382 case at.INIT: 383 await this.wallpaperSetup(true /* isStartup */); 384 this.initHighlightCounter(); 385 break; 386 case at.UNINIT: 387 break; 388 case at.SYSTEM_TICK: 389 break; 390 case at.PREF_CHANGED: 391 if ( 392 action.data.name === 393 "newtabWallpapers.newtabWallpapers.customColor.enabled" || 394 action.data.name === "newtabWallpapers.customWallpaper.enabled" || 395 action.data.name === "newtabWallpapers.enabled" 396 ) { 397 this.wallpaperTeardown(); 398 await this.wallpaperSetup(false /* isStartup */); 399 } 400 if (action.data.name === "newtabWallpapers.highlightSeenCounter") { 401 // Reset redux highlight counter to pref 402 this.initHighlightCounter(); 403 } 404 break; 405 case at.WALLPAPERS_SET: 406 break; 407 case at.WALLPAPERS_FEATURE_HIGHLIGHT_SEEN: 408 this.wallpaperSeenEvent(); 409 break; 410 case at.WALLPAPER_UPLOAD: 411 this.wallpaperUpload(action.data.file); 412 break; 413 case at.WALLPAPER_REMOVE_UPLOAD: 414 await this.removeCustomWallpaper(); 415 break; 416 } 417 } 418 }