Bug 1774397 - Add notification dot to Firefox View button. r=Gijs,sfoster,desktop-theme-reviewers,dao

Differential Revision: https://phabricator.services.mozilla.com/D151401
This commit is contained in:
Niklas Baumgardner 2022-08-01 15:19:58 +00:00
Родитель 1b18a3e794
Коммит 9e4f9f010b
13 изменённых файлов: 667 добавлений и 135 удалений

Просмотреть файл

@ -2507,6 +2507,8 @@ var gBrowserInit = {
gAccessibilityServiceIndicator.uninit();
FirefoxViewHandler.uninit();
if (gToolbarKeyNavEnabled) {
ToolbarKeyboardNavigator.uninit();
}
@ -9925,11 +9927,24 @@ var ConfirmationHint = {
var FirefoxViewHandler = {
tab: null,
init() {
const { FirefoxViewNotificationManager } = ChromeUtils.importESModule(
"resource:///modules/firefox-view-notification-manager.sys.mjs"
);
if (
AppConstants.NIGHTLY_BUILD &&
!Services.prefs.getBoolPref("browser.tabs.firefox-view")
) {
document.getElementById("menu_openFirefoxView").hidden = true;
} else {
let shouldShow = FirefoxViewNotificationManager.shouldNotificationDotBeShowing();
this.toggleNotificationDot(shouldShow);
}
Services.obs.addObserver(this, "firefoxview-notification-dot-update");
this.observerAdded = true;
},
uninit() {
if (this.observerAdded) {
Services.obs.removeObserver(this, "firefoxview-notification-dot-update");
}
},
openTab() {
@ -9937,6 +9952,7 @@ var FirefoxViewHandler = {
this.tab = gBrowser.addTrustedTab("about:firefoxview", { index: 0 });
this.tab.addEventListener("TabClose", this, { once: true });
gBrowser.tabContainer.addEventListener("TabSelect", this);
window.addEventListener("activate", this);
gBrowser.hideTab(this.tab);
}
gBrowser.selectedTab = this.tab;
@ -9952,6 +9968,25 @@ var FirefoxViewHandler = {
this.tab = null;
gBrowser.tabContainer.removeEventListener("TabSelect", this);
break;
case "activate":
if (this.tab?.selected) {
Services.obs.notifyObservers(
null,
"firefoxview-notification-dot-update",
"false"
);
}
break;
}
},
observe(sub, topic, data) {
if (topic === "firefoxview-notification-dot-update") {
let shouldShow = data === "true";
this.toggleNotificationDot(shouldShow);
}
},
toggleNotificationDot(shouldShow) {
let fxViewButton = document.getElementById("firefox-view-button");
fxViewButton?.setAttribute("attention", shouldShow);
},
};

Просмотреть файл

@ -472,6 +472,11 @@ const CustomizableWidgets = [
return Services.prefs.getBoolPref("browser.tabs.firefox-view");
},
onCommand(e) {
Services.obs.notifyObservers(
null,
"firefoxview-notification-dot-update",
"false"
);
e.view.FirefoxViewHandler.openTab();
},
},

Просмотреть файл

@ -0,0 +1,106 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
* This module exports the FirefoxViewNotificationManager singleton, which manages the notification state
* for the Firefox View button
*/
const RECENT_TABS_SYNC = "services.sync.lastTabFetch";
const lazy = {};
const { XPCOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
);
XPCOMUtils.defineLazyModuleGetters(lazy, {
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
SyncedTabs: "resource://services-sync/SyncedTabs.jsm",
});
export const FirefoxViewNotificationManager = new (class {
#currentlyShowing;
constructor() {
XPCOMUtils.defineLazyPreferenceGetter(
this,
"lastTabFetch",
RECENT_TABS_SYNC,
false,
() => {
this.handleTabSync();
}
);
// Need to access the pref variable for the observer to start observing
// See the defineLazyPreferenceGetter function header
this.lastTabFetch;
Services.obs.addObserver(this, "firefoxview-notification-dot-update");
this.#currentlyShowing = false;
}
async handleTabSync() {
let newSyncedTabs = await lazy.SyncedTabs.getRecentTabs(3);
this.#currentlyShowing = this.tabsListChanged(newSyncedTabs);
this.showNotificationDot();
this.syncedTabs = newSyncedTabs;
}
showNotificationDot() {
if (this.#currentlyShowing) {
Services.obs.notifyObservers(
null,
"firefoxview-notification-dot-update",
"true"
);
}
}
observe(sub, topic, data) {
if (topic === "firefoxview-notification-dot-update" && data === "false") {
this.#currentlyShowing = false;
}
}
tabsListChanged(newTabs) {
// The first time the tabs list is changed this.tabs is undefined because we haven't synced yet.
// We don't want to show the badge here because it's not an actual change,
// we are just syncing for the first time.
if (!this.syncedTabs) {
return false;
}
// We loop through all windows to see if any window has currentURI "about:firefoxview" and
// the window is visible because we don't want to show the notification badge in that case
for (let window of lazy.BrowserWindowTracker.orderedWindows) {
// if the url is "about:firefoxview" and the window visible we don't want to show the notification badge
if (
window.FirefoxViewHandler.tab?.selected &&
!window.isFullyOccluded &&
window.windowState !== window.STATE_MINIMIZED
) {
return false;
}
}
if (newTabs.length > this.syncedTabs.length) {
return true;
}
for (let i = 0; i < newTabs.length; i++) {
let newTab = newTabs[i];
let oldTab = this.syncedTabs[i];
if (newTab?.url !== oldTab?.url) {
return true;
}
}
return false;
}
shouldNotificationDotBeShowing() {
return this.#currentlyShowing;
}
})();

Просмотреть файл

@ -104,19 +104,7 @@ class TabPickupList extends HTMLElement {
}
async getSyncedTabData() {
let tabs = [];
let clients = await lazy.SyncedTabs.getTabClients();
for (let client of clients) {
for (let tab of client.tabs) {
tab.device = client.name;
tab.deviceType = client.clientType;
}
tabs = [...tabs, ...client.tabs.reverse()];
}
tabs = tabs
.sort((a, b) => b.lastUsed - a.lastUsed)
.slice(0, this.maxTabsLength);
let tabs = await lazy.SyncedTabs.getRecentTabs(this.maxTabsLength);
this.updateTabsList(tabs);
}

Просмотреть файл

@ -5,6 +5,7 @@ support-files = head.js
[browser_firefoxview.js]
[browser_firefoxview_tab.js]
[browser_notification_dot.js]
[browser_recently_closed_tabs.js]
[browser_setup_state.js]
[browser_tab_pickup_list.js]

Просмотреть файл

@ -3,7 +3,6 @@
"use strict";
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
const { BuiltInThemes } = ChromeUtils.import(
"resource:///modules/BuiltInThemes.jsm"
);

Просмотреть файл

@ -0,0 +1,314 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const tabsList1 = syncedTabsData1[0].tabs;
const tabsList2 = syncedTabsData1[1].tabs;
const BADGE_TOP_RIGHT = "75% 25%";
const { SyncedTabs } = ChromeUtils.import(
"resource://services-sync/SyncedTabs.jsm"
);
function setupRecentDeviceListMocks() {
const sandbox = sinon.createSandbox();
sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => [
{
id: 1,
name: "My desktop",
isCurrentDevice: true,
type: "desktop",
},
{
id: 2,
name: "My iphone",
type: "mobile",
},
]);
sandbox.stub(UIState, "get").returns({
status: UIState.STATUS_SIGNED_IN,
syncEnabled: true,
});
return sandbox;
}
function waitForWindowActive(win, active) {
return Promise.all([
BrowserTestUtils.waitForEvent(win, active ? "focus" : "blur"),
BrowserTestUtils.waitForEvent(win, active ? "activate" : "deactivate"),
]);
}
async function waitForNotificationBadgeToBeShowing(fxViewButton) {
await BrowserTestUtils.waitForMutationCondition(
fxViewButton,
{ attributes: true },
() => fxViewButton.getAttribute("attention") === "true"
);
return fxViewButton.getAttribute("attention") === "true";
}
async function waitForNotificationBadgeToBeHidden(fxViewButton) {
await BrowserTestUtils.waitForMutationCondition(
fxViewButton,
{ attributes: true },
() =>
!fxViewButton.getAttribute("attention") ||
fxViewButton.getAttribute("attention") === "false"
);
return (
!fxViewButton.getAttribute("attention") ||
fxViewButton.getAttribute("attention") === "false"
);
}
function getBackgroundPositionForElement(ele) {
let style = getComputedStyle(ele);
return style.getPropertyValue("background-position");
}
let recentFetchTime = Math.floor(Date.now() / 1000);
async function initTabSync() {
recentFetchTime += 1;
info("updating lastFetch:" + recentFetchTime);
Services.prefs.setIntPref("services.sync.lastTabFetch", recentFetchTime);
await TestUtils.waitForTick();
}
add_setup(async () => {
await window.delayedStartupPromise;
await addFirefoxViewButtonToToolbar();
registerCleanupFunction(() => {
BrowserTestUtils.removeTab(FirefoxViewHandler.tab);
removeFirefoxViewButtonFromToolbar();
});
});
/**
* Test that the notification badge will show and hide in the correct cases
*/
add_task(async function testNotificationDot() {
const sandbox = setupRecentDeviceListMocks();
const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
syncedTabsMock.returns(tabsList1);
// Initiate a synced tabs update with new tabs
await initTabSync();
let fxViewBtn = document.getElementById("firefox-view-button");
ok(fxViewBtn, "Got the Firefox View button");
ok(
BrowserTestUtils.is_visible(fxViewBtn),
"The Firefox View button is showing"
);
ok(
await waitForNotificationBadgeToBeHidden(fxViewBtn),
"The notification badge is not showing initially"
);
syncedTabsMock.returns(tabsList2);
// Initiate a synced tabs update with new tabs
await initTabSync();
ok(
await waitForNotificationBadgeToBeShowing(fxViewBtn),
"The notification badge is showing after first tab sync"
);
// check that switching to the firefoxviewtab removes the badge
fxViewBtn.click();
ok(
await waitForNotificationBadgeToBeHidden(fxViewBtn),
"The notification badge is not showing after going to Firefox View"
);
syncedTabsMock.returns(tabsList1);
// Initiate a synced tabs update with new tabs
await initTabSync();
// The noti badge would show but we are on a Firefox View page so no need to show the noti badge
ok(
await waitForNotificationBadgeToBeHidden(fxViewBtn),
"The notification badge is not showing after tab sync while Firefox View is focused"
);
let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
syncedTabsMock.returns(tabsList2);
await initTabSync();
ok(
await waitForNotificationBadgeToBeShowing(fxViewBtn),
"The notification badge is showing after navigation to a new tab"
);
// check that switching back to the Firefox View tab removes the badge
fxViewBtn.click();
ok(
await waitForNotificationBadgeToBeHidden(fxViewBtn),
"The notification badge is not showing after focusing the Firefox View tab"
);
await BrowserTestUtils.switchTab(gBrowser, newTab);
// Initiate a synced tabs update with no new tabs
await initTabSync();
ok(
await waitForNotificationBadgeToBeHidden(fxViewBtn),
"The notification badge is not showing after a tab sync with the same tabs"
);
BrowserTestUtils.removeTab(gBrowser.selectedTab);
BrowserTestUtils.removeTab(newTab);
sandbox.restore();
});
/**
* Tests the notification badge with multiple windows
*/
add_task(async function testNotificationDotOnMultipleWindows() {
const sandbox = setupRecentDeviceListMocks();
const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
syncedTabsMock.returns(tabsList1);
// Initiate a synced tabs update
await initTabSync();
let fxViewBtn = document.getElementById("firefox-view-button");
ok(fxViewBtn, "Got the Firefox View button");
// Create a new window
let win = await BrowserTestUtils.openNewBrowserWindow();
await win.delayedStartupPromise;
let fxViewBtn2 = win.document.getElementById("firefox-view-button");
fxViewBtn2.click();
// Make sure the badge doesn't shows on all windows
ok(
await waitForNotificationBadgeToBeHidden(fxViewBtn),
"The notification badge is not showing in the inital window"
);
ok(
await waitForNotificationBadgeToBeHidden(fxViewBtn2),
"The notification badge is not showing in the second window"
);
// Minimize the window.
win.minimize();
await TestUtils.waitForCondition(
() => !win.gBrowser.selectedTab.linkedBrowser.docShellIsActive
);
syncedTabsMock.returns(tabsList2);
// Initiate a synced tabs update with new tabs
await initTabSync();
// The badge will show because the View tab is minimized
// Make sure the badge shows on all windows
ok(
await waitForNotificationBadgeToBeShowing(fxViewBtn),
"The notification badge is showing in the initial window"
);
ok(
await waitForNotificationBadgeToBeShowing(fxViewBtn2),
"The notification badge is showing in the second window"
);
win.restore();
await TestUtils.waitForCondition(
() => win.gBrowser.selectedTab.linkedBrowser.docShellIsActive
);
await BrowserTestUtils.closeWindow(win);
sandbox.restore();
});
/**
* Tests the notification badge is in the correct spot and that the badge shows when opening a new window
* if another window is showing the badge
*/
add_task(async function testNotificationDotLocation() {
const sandbox = setupRecentDeviceListMocks();
const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
syncedTabsMock.returns(tabsList1);
// Initiate a synced tabs update
await initTabSync();
let fxViewBtn = document.getElementById("firefox-view-button");
ok(fxViewBtn, "Got the Firefox View button");
syncedTabsMock.returns(tabsList2);
// Initiate a synced tabs update
await initTabSync();
ok(
await waitForNotificationBadgeToBeShowing(fxViewBtn),
"The notification badge is showing initially"
);
// Create a new window
let win = await BrowserTestUtils.openNewBrowserWindow();
await win.delayedStartupPromise;
// let newTab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, "");
// Make sure the badge doesn't showing on the new window
let fxViewBtn2 = win.document.getElementById("firefox-view-button");
ok(
await waitForNotificationBadgeToBeShowing(fxViewBtn2),
"The notification badge is showing in the second window after opening"
);
// Make sure the badge is below and center now
isnot(
getBackgroundPositionForElement(fxViewBtn),
BADGE_TOP_RIGHT,
"The notification badge is not showing in the top right in the initial window"
);
isnot(
getBackgroundPositionForElement(fxViewBtn2),
BADGE_TOP_RIGHT,
"The notification badge is not showing in the top right in the second window"
);
CustomizableUI.addWidgetToArea(
"firefox-view-button",
CustomizableUI.AREA_NAVBAR
);
// Make sure both windows still have the notification badge
ok(
await waitForNotificationBadgeToBeShowing(fxViewBtn),
"The notification badge is showing in the initial window"
);
ok(
await waitForNotificationBadgeToBeShowing(fxViewBtn2),
"The notification badge is showing in the second window"
);
// Make sure the badge is in the top right now
is(
getBackgroundPositionForElement(fxViewBtn),
BADGE_TOP_RIGHT,
"The notification badge is showing in the top right in the initial window"
);
is(
getBackgroundPositionForElement(fxViewBtn2),
BADGE_TOP_RIGHT,
"The notification badge is showing in the top right in the second window"
);
await BrowserTestUtils.closeWindow(win);
sandbox.restore();
});

Просмотреть файл

@ -1,15 +1,13 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const { UIState } = ChromeUtils.import("resource://services-sync/UIState.jsm");
const { TabsSetupFlowManager } = ChromeUtils.importESModule(
"resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
);
const MOBILE_PROMO_DISMISSED_PREF =
"browser.tabs.firefox-view.mobilePromo.dismissed";
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
var gMockFxaDevices = null;
function promiseSyncReady() {

Просмотреть файл

@ -1,65 +1,13 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const { UIState } = ChromeUtils.import("resource://services-sync/UIState.jsm");
const { TabsSetupFlowManager } = ChromeUtils.importESModule(
"resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
);
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
XPCOMUtils.defineLazyModuleGetters(globalThis, {
SyncedTabs: "resource://services-sync/SyncedTabs.jsm",
});
const sandbox = sinon.createSandbox();
const syncedTabsData1 = [
{
id: 1,
type: "client",
name: "My desktop",
clientType: "desktop",
lastModified: 1655730486760,
tabs: [
{
type: "tab",
title: "Sandboxes - Sinon.JS",
url: "https://sinonjs.org/releases/latest/sandbox/",
icon: "https://sinonjs.org/assets/images/favicon.png",
lastUsed: 1655391592, // Thu Jun 16 2022 14:59:52 GMT+0000
},
{
type: "tab",
title: "Internet for people, not profits - Mozilla",
url: "https://www.mozilla.org/",
icon:
"https://www.mozilla.org/media/img/favicons/mozilla/favicon.d25d81d39065.ico",
lastUsed: 1655730486, // Mon Jun 20 2022 13:08:06 GMT+0000
},
],
},
{
id: 2,
type: "client",
name: "My iphone",
clientType: "mobile",
lastModified: 1655727832930,
tabs: [
{
type: "tab",
title: "The Guardian",
url: "https://www.theguardian.com/",
icon: "page-icon:https://www.theguardian.com/",
lastUsed: 1655291890, // Wed Jun 15 2022 11:18:10 GMT+0000
},
{
type: "tab",
title: "The Times",
url: "https://www.thetimes.co.uk/",
icon: "page-icon:https://www.thetimes.co.uk/",
lastUsed: 1655727485, // Mon Jun 20 2022 12:18:05 GMT+0000
},
],
},
];
const twoTabs = [
{
@ -122,60 +70,7 @@ const syncedTabsData5 = [
},
];
function setupMocks(mockData1, mockData2) {
const mockDeviceData = [
{
id: 1,
name: "My desktop",
isCurrentDevice: true,
type: "desktop",
},
{
id: 2,
name: "My iphone",
type: "mobile",
},
];
sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => mockDeviceData);
sandbox.stub(UIState, "get").returns({
status: UIState.STATUS_SIGNED_IN,
syncEnabled: true,
});
const syncedTabsMock = sandbox.stub(SyncedTabs, "getTabClients");
syncedTabsMock.onFirstCall().returns(mockData1);
syncedTabsMock.onSecondCall().returns(mockData2);
}
async function setupListState(browser) {
// Skip the synced tabs sign up flow to get to a loaded list state
await SpecialPowers.pushPrefEnv({
set: [["services.sync.engine.tabs", true]],
});
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
const recentFetchTime = Math.floor(Date.now() / 1000);
info("updating lastFetch:" + recentFetchTime);
Services.prefs.setIntPref("services.sync.lastTabFetch", recentFetchTime);
await waitForElementVisible(browser, "#tabpickup-steps", false);
await waitForElementVisible(browser, "#tabpickup-tabs-container", true);
const tabsContainer = browser.contentWindow.document.querySelector(
"#tabpickup-tabs-container"
);
await BrowserTestUtils.waitForMutationCondition(
tabsContainer,
{ attributeFilter: ["class"], attributes: true },
() => {
return !tabsContainer.classList.contains("loading");
}
);
}
function cleanup() {
sandbox.restore();
Services.prefs.clearUserPref("services.sync.engine.tabs");
Services.prefs.clearUserPref("services.sync.lastTabFetch");
}
@ -193,7 +88,12 @@ add_task(async function test_tab_list_ordering() {
async browser => {
const { document } = browser.contentWindow;
setupMocks(syncedTabsData1, syncedTabsData2);
const sandbox = setupRecentDeviceListMocks();
const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
let mockTabs1 = getMockTabData(syncedTabsData1);
let mockTabs2 = getMockTabData(syncedTabsData2);
syncedTabsMock.returns(mockTabs1);
await setupListState(browser);
testVisibility(browser, {
@ -221,6 +121,7 @@ add_task(async function test_tab_list_ordering() {
"Last list item in synced-tabs-list is in the correct order"
);
syncedTabsMock.returns(mockTabs2);
// Initiate a synced tabs update
Services.obs.notifyObservers(null, "services.sync.tabs.changed");
@ -250,6 +151,8 @@ add_task(async function test_tab_list_ordering() {
.children[2].textContent.includes("Internet for people, not profits"),
"Last list item in synced-tabs-list has been updated"
);
sandbox.restore();
cleanup();
}
);
@ -264,7 +167,12 @@ add_task(async function test_empty_list_items() {
async browser => {
const { document } = browser.contentWindow;
setupMocks(syncedTabsData3, syncedTabsData4);
const sandbox = setupRecentDeviceListMocks();
const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
let mockTabs1 = getMockTabData(syncedTabsData3);
let mockTabs2 = getMockTabData(syncedTabsData4);
syncedTabsMock.returns(mockTabs1);
await setupListState(browser);
testVisibility(browser, {
@ -299,6 +207,7 @@ add_task(async function test_empty_list_items() {
"Last list item in synced-tabs-list should be a placeholder"
);
syncedTabsMock.returns(mockTabs2);
// Initiate a synced tabs update
Services.obs.notifyObservers(null, "services.sync.tabs.changed");
@ -327,6 +236,7 @@ add_task(async function test_empty_list_items() {
"Last list item in synced-tabs-list has been updated"
);
sandbox.restore();
cleanup();
}
);
@ -341,7 +251,12 @@ add_task(async function test_empty_list() {
async browser => {
const { document } = browser.contentWindow;
setupMocks([], syncedTabsData4);
const sandbox = setupRecentDeviceListMocks();
const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
let mockTabs1 = getMockTabData([]);
let mockTabs2 = getMockTabData(syncedTabsData4);
syncedTabsMock.returns(mockTabs1);
await setupListState(browser);
testVisibility(browser, {
@ -358,6 +273,7 @@ add_task(async function test_empty_list() {
"collapsible container should have correct styling when the list is empty"
);
syncedTabsMock.returns(mockTabs2);
// Initiate a synced tabs update
Services.obs.notifyObservers(null, "services.sync.tabs.changed");
@ -375,6 +291,7 @@ add_task(async function test_empty_list() {
},
});
sandbox.restore();
cleanup();
}
);
@ -393,7 +310,11 @@ add_task(async function test_time_updates_correctly() {
async browser => {
const { document } = browser.contentWindow;
setupMocks(syncedTabsData5, []);
const sandbox = setupRecentDeviceListMocks();
const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
let mockTabs1 = getMockTabData(syncedTabsData5);
syncedTabsMock.returns(mockTabs1);
await setupListState(browser);
ok(
@ -419,6 +340,7 @@ add_task(async function test_time_updates_correctly() {
"synced-tab-li-time text has updated"
);
sandbox.restore();
cleanup();
await SpecialPowers.popPrefEnv();
}

Просмотреть файл

@ -1,6 +1,59 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const { UIState } = ChromeUtils.import("resource://services-sync/UIState.jsm");
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
const syncedTabsData1 = [
{
id: 1,
type: "client",
name: "My desktop",
clientType: "desktop",
lastModified: 1655730486760,
tabs: [
{
type: "tab",
title: "Sandboxes - Sinon.JS",
url: "https://sinonjs.org/releases/latest/sandbox/",
icon: "https://sinonjs.org/assets/images/favicon.png",
lastUsed: 1655391592, // Thu Jun 16 2022 14:59:52 GMT+0000
},
{
type: "tab",
title: "Internet for people, not profits - Mozilla",
url: "https://www.mozilla.org/",
icon:
"https://www.mozilla.org/media/img/favicons/mozilla/favicon.d25d81d39065.ico",
lastUsed: 1655730486, // Mon Jun 20 2022 13:08:06 GMT+0000
},
],
},
{
id: 2,
type: "client",
name: "My iphone",
clientType: "mobile",
lastModified: 1655727832930,
tabs: [
{
type: "tab",
title: "The Guardian",
url: "https://www.theguardian.com/",
icon: "page-icon:https://www.theguardian.com/",
lastUsed: 1655291890, // Wed Jun 15 2022 11:18:10 GMT+0000
},
{
type: "tab",
title: "The Times",
url: "https://www.thetimes.co.uk/",
icon: "page-icon:https://www.thetimes.co.uk/",
lastUsed: 1655727485, // Mon Jun 20 2022 12:18:05 GMT+0000
},
],
},
];
/* eslint-disable no-unused-vars */
function testVisibility(browser, expected) {
const { document } = browser.contentWindow;
@ -109,3 +162,84 @@ async function withFirefoxView(
}
return Promise.resolve(result);
}
async function addFirefoxViewButtonToToolbar() {
await SpecialPowers.pushPrefEnv({
set: [["browser.tabs.firefox-view", true]],
});
info(Services.prefs.getBoolPref("browser.tabs.firefox-view", false));
CustomizableUI.addWidgetToArea(
"firefox-view-button",
CustomizableUI.AREA_TABSTRIP,
0
);
}
function removeFirefoxViewButtonFromToolbar() {
CustomizableUI.removeWidgetFromArea("firefox-view-button");
}
function setupRecentDeviceListMocks() {
const sandbox = sinon.createSandbox();
sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => [
{
id: 1,
name: "My desktop",
isCurrentDevice: true,
type: "desktop",
},
{
id: 2,
name: "My iphone",
type: "mobile",
},
]);
sandbox.stub(UIState, "get").returns({
status: UIState.STATUS_SIGNED_IN,
syncEnabled: true,
});
return sandbox;
}
function getMockTabData(clients) {
let tabs = [];
for (let client of clients) {
for (let tab of client.tabs) {
tab.device = client.name;
tab.deviceType = client.clientType;
}
tabs = [...tabs, ...client.tabs.reverse()];
}
tabs = tabs.sort((a, b) => b.lastUsed - a.lastUsed).slice(0, 3);
return tabs;
}
async function setupListState(browser) {
// Skip the synced tabs sign up flow to get to a loaded list state
await SpecialPowers.pushPrefEnv({
set: [["services.sync.engine.tabs", true]],
});
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
const recentFetchTime = Math.floor(Date.now() / 1000);
info("updating lastFetch:" + recentFetchTime);
Services.prefs.setIntPref("services.sync.lastTabFetch", recentFetchTime);
await waitForElementVisible(browser, "#tabpickup-steps", false);
await waitForElementVisible(browser, "#tabpickup-tabs-container", true);
const tabsContainer = browser.contentWindow.document.querySelector(
"#tabpickup-tabs-container"
);
await BrowserTestUtils.waitForMutationCondition(
tabsContainer,
{ attributeFilter: ["class"], attributes: true },
() => {
return !tabsContainer.classList.contains("loading");
}
);
}

Просмотреть файл

@ -30,7 +30,7 @@
#TabsToolbar:not(:-moz-lwtheme),
#TabsToolbar[brighttext]:not(:-moz-lwtheme) {
--attention-icon-color: AccentColor;
--tab-attention-icon-color: AccentColor;
}
:root:-moz-lwtheme {

Просмотреть файл

@ -15,6 +15,7 @@
/* --tabpanel-background-color matches $in-content-page-background in newtab
(browser/components/newtab/content-src/styles/_variables.scss) */
--tabpanel-background-color: #F9F9FB;
--tab-attention-icon-color: var(--lwt-tab-attention-icon-color, rgb(42,195,162));
}
#tabbrowser-tabpanels browser {
@ -623,17 +624,14 @@
/* Pinned tabs */
#TabsToolbar {
--attention-icon-color: var(--lwt-tab-attention-icon-color, rgb(42,195,162));
}
#TabsToolbar[brighttext] {
--attention-icon-color: var(--lwt-tab-attention-icon-color, rgb(84,255,189));
toolbar[brighttext] {
--tab-attention-icon-color: var(--lwt-tab-attention-icon-color, rgb(84,255,189));
}
.tabbrowser-tab:is([image], [pinned]) > .tab-stack > .tab-content[attention]:not([selected="true"]),
.tabbrowser-tab > .tab-stack > .tab-content[pinned][titlechanged]:not([selected="true"]) {
background-image: radial-gradient(circle, var(--attention-icon-color), var(--attention-icon-color) 2px, transparent 2px);
.tabbrowser-tab > .tab-stack > .tab-content[pinned][titlechanged]:not([selected="true"]),
#firefox-view-button[attention="true"] {
background-image: radial-gradient(circle, var(--tab-attention-icon-color), var(--tab-attention-icon-color) 2px, transparent 2px);
background-position: center bottom calc(6.5px + var(--tabs-navbar-shadow-size));
background-size: 4px 4px;
background-repeat: no-repeat;
@ -693,6 +691,23 @@
display: none;
}
toolbar:not(#TabsToolbar) #firefox-view-button {
background-position: top 25% right 25%;
}
:is(#widget-overflow-mainView, #widget-overflow-fixed-list) #firefox-view-button {
background-position: top 25% left 22px;
}
/* RTL notification dot */
toolbar:not(#TabsToolbar) #firefox-view-button:-moz-locale-dir(rtl) {
background-position-x: left 25%;
}
:is(#widget-overflow-mainView, #widget-overflow-fixed-list) #firefox-view-button:-moz-locale-dir(rtl) {
background-position-x: right 22px;
}
/* Tab bar scroll arrows */
#tabbrowser-arrowscrollbox::part(scrollbutton-up),

Просмотреть файл

@ -292,4 +292,19 @@ var SyncedTabs = {
extraOptions
);
},
async getRecentTabs(maxCount) {
let tabs = [];
let clients = await this.getTabClients();
for (let client of clients) {
for (let tab of client.tabs) {
tab.device = client.name;
tab.deviceType = client.clientType;
}
tabs = [...tabs, ...client.tabs.reverse()];
}
tabs = tabs.sort((a, b) => b.lastUsed - a.lastUsed).slice(0, maxCount);
return tabs;
},
};