Files
tubestation/waterfox/browser/components/sidebar/common/tabs-update.js
2025-11-06 14:13:52 +00:00

445 lines
16 KiB
JavaScript

/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is the Tree Style Tab.
*
* The Initial Developer of the Original Code is YUKI "Piro" Hiroshi.
* Portions created by the Initial Developer are Copyright (C) 2011-2025
* the Initial Developer. All Rights Reserved.
*
* Contributor(s): YUKI "Piro" Hiroshi <piro.outsider.reflex@gmail.com>
* wanabe <https://github.com/wanabe>
* Tetsuharu OHZEKI <https://github.com/saneyuki>
* Xidorn Quan <https://github.com/upsuper> (Firefox 40+ support)
* lv7777 (https://github.com/lv7777)
*
* ***** END LICENSE BLOCK ******/
'use strict';
import {
log as internalLogger,
dumpTab,
wait,
mapAndFilter
} from './common.js';
import * as Constants from './constants.js';
import * as ContextualIdentities from './contextual-identities.js';
import * as SidebarConnection from './sidebar-connection.js';
import * as TabsStore from './tabs-store.js';
import { Tab } from './TreeItem.js';
function log(...args) {
internalLogger('common/tabs-update', ...args);
}
const mBufferedUpdates = new Map();
function getBufferedUpdate(tab) {
const update = mBufferedUpdates.get(tab.id) || {
windowId: tab.windowId,
tabId: tab.id,
attributes: {
updated: {},
added: {},
removed: new Set(),
},
isGroupTab: false,
updatedTitle: undefined,
updatedLabel: undefined,
favIconUrl: undefined,
loadingState: undefined,
loadingStateReallyChanged: undefined,
pinned: undefined,
hidden: undefined,
groupId: undefined,
soundStateChanged: false,
};
mBufferedUpdates.set(tab.id, update);
return update;
}
function flushBufferedUpdates() {
if (!Constants.IS_BACKGROUND) {
mBufferedUpdates.clear();
return;
}
const triedAt = `${Date.now()}-${parseInt(Math.random() * 65000)}`;
flushBufferedUpdates.triedAt = triedAt;
(Constants.IS_BACKGROUND ?
setTimeout : // because window.requestAnimationFrame is decelerate for an invisible document.
window.requestAnimationFrame)(() => {
if (flushBufferedUpdates.triedAt != triedAt)
return;
for (const update of mBufferedUpdates.values()) {
// no need to notify attributes broadcasted via Tab.broadcastState()
delete update.attributes.updated.highlighted;
delete update.attributes.updated.hidden;
delete update.attributes.updated.pinned;
delete update.attributes.updated.audible;
delete update.attributes.updated.mutedInfo;
delete update.attributes.updated.sharingState;
delete update.attributes.updated.incognito;
delete update.attributes.updated.attention;
delete update.attributes.updated.discarded;
if (Object.keys(update.attributes.updated).length > 0 ||
Object.keys(update.attributes.added).length > 0 ||
update.attributes.removed.size > 0 ||
update.soundStateChanged ||
update.sharingStateChanged)
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_NOTIFY_TAB_UPDATED,
windowId: update.windowId,
tabId: update.tabId,
updatedProperties: update.attributes.updated,
addedAttributes: update.attributes.added,
removedAttributes: [...update.attributes.removed],
soundStateChanged: update.soundStateChanged,
sharingStateChanged: update.sharingStateChanged,
});
// SidebarConnection.sendMessage() has its own bulk-send mechanism,
// so we don't need to bundle them like an array.
if (update.isGroupTab)
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_NOTIFY_GROUP_TAB_DETECTED,
windowId: update.windowId,
tabId: update.tabId,
});
if (update.updatedTitle !== undefined)
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_NOTIFY_TREE_ITEM_LABEL_UPDATED,
windowId: update.windowId,
tabId: update.tabId,
title: update.updatedTitle,
label: update.updatedLabel,
});
if (update.favIconUrl !== undefined)
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_NOTIFY_TAB_FAVICON_UPDATED,
windowId: update.windowId,
tabId: update.tabId,
favIconUrl: update.favIconUrl,
});
if (update.loadingState !== undefined)
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_UPDATE_LOADING_STATE,
windowId: update.windowId,
tabId: update.tabId,
status: update.loadingState,
reallyChanged: update.loadingStateReallyChanged,
});
if (update.pinned !== undefined)
SidebarConnection.sendMessage({
type: update.pinned ? Constants.kCOMMAND_NOTIFY_TAB_PINNED : Constants.kCOMMAND_NOTIFY_TAB_UNPINNED,
windowId: update.windowId,
tabId: update.tabId,
});
if (update.hidden !== undefined)
SidebarConnection.sendMessage({
type: update.hidden ? Constants.kCOMMAND_NOTIFY_TAB_HIDDEN : Constants.kCOMMAND_NOTIFY_TAB_SHOWN,
windowId: update.windowId,
tabId: update.tabId,
});
}
mBufferedUpdates.clear();
}, 0);
}
export function updateTab(tab, newState = {}, options = {}) {
const update = getBufferedUpdate(tab);
const oldState = options.old || {};
if ('url' in newState) {
tab.$TST.setAttribute(Constants.kCURRENT_URI, update.attributes.added[Constants.kCURRENT_URI] = newState.url);
update.attributes.removed.delete(Constants.kCURRENT_URI);
}
if ('url' in newState &&
newState.url.indexOf(Constants.kGROUP_TAB_URI) == 0) {
tab.$TST.addState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
update.isGroupTab = true;
Tab.onGroupTabDetected.dispatch(tab);
}
else if (tab.$TST.states.has(Constants.kTAB_STATE_GROUP_TAB) &&
!tab.$TST.hasGroupTabURL) {
tab.$TST.removeState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
update.isGroupTab = false;
}
if (options.forceApply ||
('title' in newState &&
newState.title != oldState.title)) {
if (options.forceApply) {
tab.$TST.getPermanentStates().then(states => {
tab.$TST.toggleState(
Constants.kTAB_STATE_UNREAD,
(states.includes(Constants.kTAB_STATE_UNREAD) &&
!tab.$TST.isGroupTab),
{ permanently: true }
);
});
}
else if (tab.$TST.isGroupTab) {
tab.$TST.removeState(Constants.kTAB_STATE_UNREAD, { permanently: true });
}
else if (!tab.active) {
tab.$TST.addState(Constants.kTAB_STATE_UNREAD, { permanently: true });
}
tab.$TST.label = newState.title;
Tab.onLabelUpdated.dispatch(tab);
update.updatedTitle = tab.title;
update.updatedLabel = tab.$TST.label;
}
const openerOfGroupTab = tab.$TST.isGroupTab && Tab.getOpenerFromGroupTab(tab);
if (openerOfGroupTab &&
openerOfGroupTab.favIconUrl) {
update.favIconUrl = openerOfGroupTab.favIconUrl;
}
else if (options.forceApply ||
'favIconUrl' in newState) {
tab.$TST.setAttribute(Constants.kCURRENT_FAVICON_URI, update.attributes.added[Constants.kCURRENT_FAVICON_URI] = tab.favIconUrl);
update.attributes.removed.delete(Constants.kCURRENT_FAVICON_URI);
// "favIconUrl" will be "undefined" if the website has no favicon.
// Keys with "undefined" value will be removed from JSON-stringified result,
// so we need to use "null" instead of it.
// See also: https://github.com/piroor/treestyletab/issues/3515
update.favIconUrl = tab.favIconUrl || null;
}
else if (tab.$TST.isGroupTab) {
// "about:ws-group" can set error icon for the favicon and
// reloading doesn't cloear that, so we need to clear favIconUrl manually.
tab.favIconUrl = null;
delete update.attributes.added[Constants.kCURRENT_FAVICON_URI];
update.attributes.removed.add(Constants.kCURRENT_URI);
tab.$TST.removeAttribute(Constants.kCURRENT_FAVICON_URI);
update.favIconUrl = null;
}
if ('status' in newState) {
const reallyChanged = !tab.$TST.states.has(newState.status);
const removed = newState.status == 'loading' ? 'complete' : 'loading';
tab.$TST.removeState(removed);
tab.$TST.addState(newState.status);
if (!options.forceApply) {
update.loadingState = tab.status;
update.loadingStateReallyChanged = reallyChanged;
}
}
if ((options.forceApply ||
'pinned' in newState) &&
newState.pinned != tab.$TST.states.has(Constants.kTAB_STATE_PINNED)) {
if (newState.pinned) {
tab.$TST.addState(Constants.kTAB_STATE_PINNED);
tab.$TST.removeAttribute(Constants.kLEVEL); // don't indent pinned tabs!
delete update.attributes.added[Constants.kLEVEL];
update.attributes.removed.add(Constants.kLEVEL);
Tab.onPinned.dispatch(tab);
update.pinned = true;
}
else {
tab.$TST.removeState(Constants.kTAB_STATE_PINNED);
Tab.onUnpinned.dispatch(tab);
update.pinned = false;
}
}
if (options.forceApply ||
'audible' in newState)
tab.$TST.toggleState(Constants.kTAB_STATE_AUDIBLE, newState.audible);
let soundStateChanged = false;
if (options.forceApply ||
'mutedInfo' in newState) {
soundStateChanged = true;
const muted = newState.mutedInfo?.muted;
tab.$TST.toggleState(Constants.kTAB_STATE_MUTED, muted, newState.audible);
Tab.onMutedStateChanged.dispatch(tab, muted);
}
if (options.forceApply ||
soundStateChanged ||
'audible' in newState) {
soundStateChanged = true;
tab.$TST.toggleState(
Constants.kTAB_STATE_SOUND_PLAYING,
(tab.audible &&
!tab.mutedInfo.muted)
);
}
if (soundStateChanged) {
const parent = tab.$TST.parent;
if (parent)
parent.$TST.inheritSoundStateFromChildren();
}
let sharingStateChanged = false;
if (options.forceApply ||
'sharingState' in newState) {
sharingStateChanged = true;
const sharingCamera = !!newState.sharingState?.camera;
const sharingMicrophone = !!newState.sharingState?.microphone;
const sharingScreen = !!newState.sharingState?.screen;
tab.$TST.toggleState(Constants.kTAB_STATE_SHARING_CAMERA, sharingCamera);
tab.$TST.toggleState(Constants.kTAB_STATE_SHARING_MICROPHONE, sharingMicrophone);
tab.$TST.toggleState(Constants.kTAB_STATE_SHARING_SCREEN, sharingScreen);
Tab.onSharingStateChanged.dispatch(tab, {
camera: sharingCamera,
microphone: sharingMicrophone,
screen: sharingScreen,
});
const parent = tab.$TST.parent;
if (parent)
parent.$TST.inheritSharingStateFromChildren();
}
if (options.forceApply ||
'cookieStoreId' in newState) {
for (const state of tab.$TST.states) {
if (String(state).startsWith('contextual-identity-'))
tab.$TST.removeState(state);
}
if (newState.cookieStoreId) {
const state = `contextual-identity-${newState.cookieStoreId}`;
tab.$TST.addState(state);
const identity = ContextualIdentities.get(newState.cookieStoreId);
if (identity)
tab.$TST.setAttribute(Constants.kCONTEXTUAL_IDENTITY_NAME, identity.name);
else
tab.$TST.removeAttribute(Constants.kCONTEXTUAL_IDENTITY_NAME);
}
else {
tab.$TST.removeAttribute(Constants.kCONTEXTUAL_IDENTITY_NAME);
}
}
if (options.forceApply ||
'incognito' in newState)
tab.$TST.toggleState(Constants.kTAB_STATE_PRIVATE_BROWSING, newState.incognito);
if (options.forceApply ||
'hidden' in newState) {
if (newState.hidden) {
if (!tab.$TST.states.has(Constants.kTAB_STATE_HIDDEN)) {
tab.$TST.addState(Constants.kTAB_STATE_HIDDEN);
Tab.onHidden.dispatch(tab);
update.hidden = true;
}
}
else if (tab.$TST.states.has(Constants.kTAB_STATE_HIDDEN)) {
tab.$TST.removeState(Constants.kTAB_STATE_HIDDEN);
Tab.onShown.dispatch(tab);
update.hidden = false;
}
}
if (options.forceApply ||
'highlighted' in newState)
tab.$TST.toggleState(Constants.kTAB_STATE_HIGHLIGHTED, newState.highlighted);
if (options.forceApply ||
'attention' in newState)
tab.$TST.toggleState(Constants.kTAB_STATE_ATTENTION, newState.attention);
if (options.forceApply ||
'discarded' in newState) {
wait(0).then(() => {
// Don't set this class immediately, because we need to know
// the newly active tab *was* discarded on onTabClosed handler.
tab.$TST.toggleState(Constants.kTAB_STATE_DISCARDED, newState.discarded);
});
}
if (options.forceApply ||
'groupId' in newState) {
tab.$TST.onNativeGroupModified(oldState.groupId);
update.attributes.added[Constants.kGROUP_ID] = newState.groupId;
}
update.soundStateChanged = update.soundStateChanged || soundStateChanged;
update.sharingStateChanged = update.sharingStateChanged || sharingStateChanged;
update.attributes.updated = {
...update.attributes.updated,
...(newState?.$TST?.sanitized || newState),
};
flushBufferedUpdates();
tab.$TST.invalidateCache();
}
export async function updateTabsHighlighted(highlightInfo) {
if (Tab.needToWaitTracked(highlightInfo.windowId))
await Tab.waitUntilTrackedAll(highlightInfo.windowId);
const win = TabsStore.windows.get(highlightInfo.windowId);
if (!win)
return;
//const startAt = Date.now();
const tabIds = highlightInfo.tabIds; // new Set(highlightInfo.tabIds);
const toBeUnhighlightedTabs = Tab.getHighlightedTabs(highlightInfo.windowId, {
ordered: false,
'!id': tabIds
});
const alreadyHighlightedTabs = TabsStore.highlightedTabsInWindow.get(highlightInfo.windowId);
const toBeHighlightedTabs = mapAndFilter(tabIds, id => {
const tab = win.tabs.get(id);
return tab && !alreadyHighlightedTabs.has(tab.id) && tab || undefined;
});
//console.log(`updateTabsHighlighted: ${Date.now() - startAt}ms`, { toBeHighlightedTabs, toBeUnhighlightedTabs});
const inheritToCollapsedDescendants = !!highlightInfo.inheritToCollapsedDescendants;
//log('updateTabsHighlighted ', { toBeHighlightedTabs, toBeUnhighlightedTabs});
for (const tab of toBeUnhighlightedTabs) {
TabsStore.removeHighlightedTab(tab);
updateTabHighlighted(tab, false, { inheritToCollapsedDescendants });
}
for (const tab of toBeHighlightedTabs) {
TabsStore.addHighlightedTab(tab);
updateTabHighlighted(tab, true, { inheritToCollapsedDescendants });
}
}
async function updateTabHighlighted(tab, highlighted, { inheritToCollapsedDescendants } = {}) {
log(`highlighted status of ${dumpTab(tab)}: `, { old: tab.highlighted, new: highlighted });
//if (tab.highlighted == highlighted)
// return false;
tab.$TST.toggleState(Constants.kTAB_STATE_HIGHLIGHTED, highlighted);
tab.highlighted = highlighted;
const win = TabsStore.windows.get(tab.windowId);
const inheritHighlighted = !win.tabsToBeHighlightedAlone.has(tab.id);
if (!inheritHighlighted)
win.tabsToBeHighlightedAlone.delete(tab.id);
updateTab(tab, { highlighted });
Tab.onUpdated.dispatch(tab, { highlighted }, {
inheritHighlighted: inheritToCollapsedDescendants && inheritHighlighted,
});
return true;
}
export async function completeLoadingTabs(windowId) {
const completedTabs = new Set((await browser.tabs.query({ windowId, status: 'complete' })).map(tab => tab.id));
for (const tab of Tab.getLoadingTabs(windowId, { ordered: false, iterator: true })) {
if (completedTabs.has(tab.id))
updateTab(tab, { status: 'complete' });
}
}