summaryrefslogtreecommitdiffstats
path: root/devtools/client/webconsole/reducers
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /devtools/client/webconsole/reducers
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/webconsole/reducers')
-rw-r--r--devtools/client/webconsole/reducers/autocomplete.js181
-rw-r--r--devtools/client/webconsole/reducers/filters.js32
-rw-r--r--devtools/client/webconsole/reducers/history.js244
-rw-r--r--devtools/client/webconsole/reducers/index.js34
-rw-r--r--devtools/client/webconsole/reducers/messages.js1723
-rw-r--r--devtools/client/webconsole/reducers/moz.build15
-rw-r--r--devtools/client/webconsole/reducers/notifications.js58
-rw-r--r--devtools/client/webconsole/reducers/prefs.js48
-rw-r--r--devtools/client/webconsole/reducers/ui.js137
9 files changed, 2472 insertions, 0 deletions
diff --git a/devtools/client/webconsole/reducers/autocomplete.js b/devtools/client/webconsole/reducers/autocomplete.js
new file mode 100644
index 0000000000..348ff9f7f9
--- /dev/null
+++ b/devtools/client/webconsole/reducers/autocomplete.js
@@ -0,0 +1,181 @@
+/* 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";
+
+const {
+ AUTOCOMPLETE_CLEAR,
+ AUTOCOMPLETE_DATA_RECEIVE,
+ AUTOCOMPLETE_PENDING_REQUEST,
+ AUTOCOMPLETE_RETRIEVE_FROM_CACHE,
+ EVALUATE_EXPRESSION,
+ UPDATE_HISTORY_POSITION,
+ REVERSE_SEARCH_INPUT_CHANGE,
+ REVERSE_SEARCH_BACK,
+ REVERSE_SEARCH_NEXT,
+ WILL_NAVIGATE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+function getDefaultState(overrides = {}) {
+ return Object.freeze({
+ cache: null,
+ matches: [],
+ matchProp: null,
+ isElementAccess: false,
+ pendingRequestId: null,
+ isUnsafeGetter: false,
+ getterPath: null,
+ authorizedEvaluations: [],
+ ...overrides,
+ });
+}
+
+function autocomplete(state = getDefaultState(), action) {
+ switch (action.type) {
+ case WILL_NAVIGATE:
+ return getDefaultState();
+ case AUTOCOMPLETE_RETRIEVE_FROM_CACHE:
+ return autoCompleteRetrieveFromCache(state, action);
+ case AUTOCOMPLETE_PENDING_REQUEST:
+ return {
+ ...state,
+ cache: null,
+ pendingRequestId: action.id,
+ };
+ case AUTOCOMPLETE_DATA_RECEIVE:
+ if (action.id !== state.pendingRequestId) {
+ return state;
+ }
+
+ if (action.data.matches === null) {
+ return getDefaultState();
+ }
+
+ if (action.data.isUnsafeGetter) {
+ // We only want to display the getter confirm popup if the last char is a dot or
+ // an opening bracket, or if the user forced the autocompletion with Ctrl+Space.
+ if (
+ action.input.endsWith(".") ||
+ action.input.endsWith("[") ||
+ action.force
+ ) {
+ return {
+ ...getDefaultState(),
+ isUnsafeGetter: true,
+ getterPath: action.data.getterPath,
+ authorizedEvaluations: action.authorizedEvaluations,
+ };
+ }
+
+ return {
+ ...state,
+ pendingRequestId: null,
+ };
+ }
+
+ return {
+ ...state,
+ authorizedEvaluations: action.authorizedEvaluations,
+ getterPath: null,
+ isUnsafeGetter: false,
+ pendingRequestId: null,
+ cache: {
+ input: action.input,
+ frameActorId: action.frameActorId,
+ ...action.data,
+ },
+ ...action.data,
+ };
+ // Reset the autocomplete data when:
+ // - clear is explicitely called
+ // - the user navigates the history
+ // - or an expression was evaluated.
+ case AUTOCOMPLETE_CLEAR:
+ return getDefaultState({
+ authorizedEvaluations: state.authorizedEvaluations,
+ });
+ case EVALUATE_EXPRESSION:
+ case UPDATE_HISTORY_POSITION:
+ case REVERSE_SEARCH_INPUT_CHANGE:
+ case REVERSE_SEARCH_BACK:
+ case REVERSE_SEARCH_NEXT:
+ return getDefaultState();
+ }
+
+ return state;
+}
+
+/**
+ * Retrieve from cache action reducer.
+ *
+ * @param {Object} state
+ * @param {Object} action
+ * @returns {Object} new state.
+ */
+function autoCompleteRetrieveFromCache(state, action) {
+ const { input } = action;
+ const { cache } = state;
+
+ let filterBy = input;
+ if (cache.isElementAccess) {
+ // if we're performing an element access, we can simply retrieve whatever comes
+ // after the last opening bracket.
+ filterBy = input.substring(input.lastIndexOf("[") + 1);
+ } else {
+ // Find the last non-alphanumeric other than "_", ":", or "$" if it exists.
+ const lastNonAlpha = input.match(/[^a-zA-Z0-9_$:][a-zA-Z0-9_$:]*$/);
+ // If input contains non-alphanumerics, use the part after the last one
+ // to filter the cache.
+ if (lastNonAlpha) {
+ filterBy = input.substring(input.lastIndexOf(lastNonAlpha) + 1);
+ }
+ }
+ const stripWrappingQuotes = s =>
+ s.replace(/^['"`](.+(?=['"`]$))['"`]$/g, "$1");
+ const filterByLc = filterBy.toLocaleLowerCase();
+ const looseMatching =
+ !filterBy || filterBy[0].toLocaleLowerCase() === filterBy[0];
+ const needStripQuote = cache.isElementAccess && !/^[`"']/.test(filterBy);
+ const newList = cache.matches.filter(l => {
+ if (needStripQuote) {
+ l = stripWrappingQuotes(l);
+ }
+
+ if (looseMatching) {
+ return l.toLocaleLowerCase().startsWith(filterByLc);
+ }
+
+ return l.startsWith(filterBy);
+ });
+
+ newList.sort((a, b) => {
+ const startingQuoteRegex = /^('|"|`)/;
+ const aFirstMeaningfulChar = startingQuoteRegex.test(a) ? a[1] : a[0];
+ const bFirstMeaningfulChar = startingQuoteRegex.test(b) ? b[1] : b[0];
+ const lA =
+ aFirstMeaningfulChar.toLocaleLowerCase() === aFirstMeaningfulChar;
+ const lB =
+ bFirstMeaningfulChar.toLocaleLowerCase() === bFirstMeaningfulChar;
+ if (lA === lB) {
+ if (a === filterBy) {
+ return -1;
+ }
+ if (b === filterBy) {
+ return 1;
+ }
+ return a.localeCompare(b);
+ }
+ return lA ? -1 : 1;
+ });
+
+ return {
+ ...state,
+ isUnsafeGetter: false,
+ getterPath: null,
+ matches: newList,
+ matchProp: filterBy,
+ isElementAccess: cache.isElementAccess,
+ };
+}
+
+exports.autocomplete = autocomplete;
diff --git a/devtools/client/webconsole/reducers/filters.js b/devtools/client/webconsole/reducers/filters.js
new file mode 100644
index 0000000000..61b993ada8
--- /dev/null
+++ b/devtools/client/webconsole/reducers/filters.js
@@ -0,0 +1,32 @@
+/* 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";
+
+const constants = require("resource://devtools/client/webconsole/constants.js");
+
+const FilterState = overrides =>
+ Object.freeze(cloneState(constants.DEFAULT_FILTERS_VALUES, overrides));
+
+function filters(state = FilterState(), action) {
+ switch (action.type) {
+ case constants.FILTER_TOGGLE:
+ const { filter } = action;
+ const active = !state[filter];
+ return cloneState(state, { [filter]: active });
+ case constants.FILTERS_CLEAR:
+ return FilterState();
+ case constants.FILTER_TEXT_SET:
+ const { text } = action;
+ return cloneState(state, { [constants.FILTERS.TEXT]: text });
+ }
+
+ return state;
+}
+
+function cloneState(state, overrides) {
+ return Object.assign({}, state, overrides);
+}
+
+exports.FilterState = FilterState;
+exports.filters = filters;
diff --git a/devtools/client/webconsole/reducers/history.js b/devtools/client/webconsole/reducers/history.js
new file mode 100644
index 0000000000..adfca885c5
--- /dev/null
+++ b/devtools/client/webconsole/reducers/history.js
@@ -0,0 +1,244 @@
+/* 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";
+
+const {
+ APPEND_TO_HISTORY,
+ CLEAR_HISTORY,
+ EVALUATE_EXPRESSION,
+ HISTORY_LOADED,
+ UPDATE_HISTORY_POSITION,
+ HISTORY_BACK,
+ HISTORY_FORWARD,
+ REVERSE_SEARCH_INPUT_TOGGLE,
+ REVERSE_SEARCH_INPUT_CHANGE,
+ REVERSE_SEARCH_BACK,
+ REVERSE_SEARCH_NEXT,
+ SET_TERMINAL_INPUT,
+ SET_TERMINAL_EAGER_RESULT,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+/**
+ * Create default initial state for this reducer.
+ */
+function getInitialState() {
+ return {
+ // Array with history entries
+ entries: [],
+
+ // Holds position (index) in history entries that the user is
+ // currently viewing. This is reset to this.entries.length when
+ // APPEND_TO_HISTORY action is fired.
+ position: undefined,
+
+ // Backups the original user value (if any) that can be set in
+ // the input field. It might be used again if the user doesn't
+ // pick up anything from the history and wants to return all
+ // the way back to see the original input text.
+ originalUserValue: null,
+
+ reverseSearchEnabled: false,
+ currentReverseSearchResults: null,
+ currentReverseSearchResultsPosition: null,
+
+ terminalInput: null,
+ terminalEagerResult: null,
+ };
+}
+
+function history(state = getInitialState(), action, prefsState) {
+ switch (action.type) {
+ case APPEND_TO_HISTORY:
+ case EVALUATE_EXPRESSION:
+ return appendToHistory(state, prefsState, action.expression);
+ case CLEAR_HISTORY:
+ return clearHistory(state);
+ case HISTORY_LOADED:
+ return historyLoaded(state, action.entries);
+ case UPDATE_HISTORY_POSITION:
+ return updateHistoryPosition(state, action.direction, action.expression);
+ case REVERSE_SEARCH_INPUT_TOGGLE:
+ return reverseSearchInputToggle(state, action);
+ case REVERSE_SEARCH_INPUT_CHANGE:
+ return reverseSearchInputChange(state, action.value);
+ case REVERSE_SEARCH_BACK:
+ return reverseSearchBack(state);
+ case REVERSE_SEARCH_NEXT:
+ return reverseSearchNext(state);
+ case SET_TERMINAL_INPUT:
+ return setTerminalInput(state, action.expression);
+ case SET_TERMINAL_EAGER_RESULT:
+ return setTerminalEagerResult(state, action.result);
+ }
+ return state;
+}
+
+function appendToHistory(state, prefsState, expression) {
+ // Clone state
+ state = { ...state };
+ state.entries = [...state.entries];
+
+ // Append new expression only if it isn't the same as
+ // the one recently added.
+ if (expression.trim() != state.entries[state.entries.length - 1]) {
+ state.entries.push(expression);
+ }
+
+ // Remove entries if the limit is reached
+ if (state.entries.length > prefsState.historyCount) {
+ state.entries.splice(0, state.entries.length - prefsState.historyCount);
+ }
+
+ state.position = state.entries.length;
+ state.originalUserValue = null;
+
+ return state;
+}
+
+function clearHistory(state) {
+ return getInitialState();
+}
+
+/**
+ * Handling HISTORY_LOADED action that is fired when history
+ * entries created in previous Firefox session are loaded
+ * from async-storage.
+ *
+ * Loaded entries are appended before the ones that were
+ * added to the state in this session.
+ */
+function historyLoaded(state, entries) {
+ const newEntries = [...entries, ...state.entries];
+ return {
+ ...state,
+ entries: newEntries,
+ // Default position is at the end of the list
+ // (at the latest inserted item).
+ position: newEntries.length,
+ originalUserValue: null,
+ };
+}
+
+function updateHistoryPosition(state, direction, expression) {
+ // Handle UP arrow key => HISTORY_BACK
+ // Handle DOWN arrow key => HISTORY_FORWARD
+ if (direction == HISTORY_BACK) {
+ if (state.position <= 0) {
+ return state;
+ }
+
+ // Clone state
+ state = { ...state };
+
+ // Store the current input value when the user starts
+ // browsing through the history.
+ if (state.position == state.entries.length) {
+ state.originalUserValue = expression || "";
+ }
+
+ state.position--;
+ } else if (direction == HISTORY_FORWARD) {
+ if (state.position >= state.entries.length) {
+ return state;
+ }
+
+ state = {
+ ...state,
+ position: state.position + 1,
+ };
+ }
+
+ return state;
+}
+
+function reverseSearchInputToggle(state, action) {
+ const { initialValue = "" } = action;
+
+ // We're going to close the reverse search, let's clean the state
+ if (state.reverseSearchEnabled) {
+ return {
+ ...state,
+ reverseSearchEnabled: false,
+ position: undefined,
+ currentReverseSearchResults: null,
+ currentReverseSearchResultsPosition: null,
+ };
+ }
+
+ // If we're enabling the reverse search, we treat it as a reverse search input change,
+ // since we can have an initial value.
+ return reverseSearchInputChange(state, initialValue);
+}
+
+function reverseSearchInputChange(state, searchString) {
+ if (searchString === "") {
+ return {
+ ...state,
+ position: undefined,
+ currentReverseSearchResults: null,
+ currentReverseSearchResultsPosition: null,
+ };
+ }
+
+ searchString = searchString.toLocaleLowerCase();
+ const matchingEntries = state.entries.filter(entry =>
+ entry.toLocaleLowerCase().includes(searchString)
+ );
+ // We only return unique entries, but we want to keep the latest entry in the array if
+ // it's duplicated (e.g. if we have [1,2,1], we want to get [2,1], not [1,2]).
+ // To do that, we need to reverse the matching entries array, provide it to a Set,
+ // transform it back to an array and reverse it again.
+ const uniqueEntries = new Set(matchingEntries.reverse());
+ const currentReverseSearchResults = Array.from(
+ new Set(uniqueEntries)
+ ).reverse();
+
+ return {
+ ...state,
+ position: undefined,
+ currentReverseSearchResults,
+ currentReverseSearchResultsPosition: currentReverseSearchResults.length - 1,
+ };
+}
+
+function reverseSearchBack(state) {
+ let nextPosition = state.currentReverseSearchResultsPosition - 1;
+ if (nextPosition < 0) {
+ nextPosition = state.currentReverseSearchResults.length - 1;
+ }
+
+ return {
+ ...state,
+ currentReverseSearchResultsPosition: nextPosition,
+ };
+}
+
+function reverseSearchNext(state) {
+ let previousPosition = state.currentReverseSearchResultsPosition + 1;
+ if (previousPosition >= state.currentReverseSearchResults.length) {
+ previousPosition = 0;
+ }
+
+ return {
+ ...state,
+ currentReverseSearchResultsPosition: previousPosition,
+ };
+}
+
+function setTerminalInput(state, expression) {
+ return {
+ ...state,
+ terminalInput: expression,
+ terminalEagerResult: !expression ? null : state.terminalEagerResult,
+ };
+}
+
+function setTerminalEagerResult(state, result) {
+ return {
+ ...state,
+ terminalEagerResult: result,
+ };
+}
+
+exports.history = history;
diff --git a/devtools/client/webconsole/reducers/index.js b/devtools/client/webconsole/reducers/index.js
new file mode 100644
index 0000000000..4dbc9d37f0
--- /dev/null
+++ b/devtools/client/webconsole/reducers/index.js
@@ -0,0 +1,34 @@
+/* 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";
+
+const {
+ autocomplete,
+} = require("resource://devtools/client/webconsole/reducers/autocomplete.js");
+const {
+ filters,
+} = require("resource://devtools/client/webconsole/reducers/filters.js");
+const {
+ messages,
+} = require("resource://devtools/client/webconsole/reducers/messages.js");
+const {
+ prefs,
+} = require("resource://devtools/client/webconsole/reducers/prefs.js");
+const { ui } = require("resource://devtools/client/webconsole/reducers/ui.js");
+const {
+ notifications,
+} = require("resource://devtools/client/webconsole/reducers/notifications.js");
+const {
+ history,
+} = require("resource://devtools/client/webconsole/reducers/history.js");
+
+exports.reducers = {
+ autocomplete,
+ filters,
+ messages,
+ prefs,
+ ui,
+ notifications,
+ history,
+};
diff --git a/devtools/client/webconsole/reducers/messages.js b/devtools/client/webconsole/reducers/messages.js
new file mode 100644
index 0000000000..1bbad87a7a
--- /dev/null
+++ b/devtools/client/webconsole/reducers/messages.js
@@ -0,0 +1,1723 @@
+/* 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";
+
+const {
+ isGroupType,
+ isMessageNetworkError,
+ l10n,
+} = require("resource://devtools/client/webconsole/utils/messages.js");
+
+const constants = require("resource://devtools/client/webconsole/constants.js");
+const { DEFAULT_FILTERS, FILTERS, MESSAGE_TYPE, MESSAGE_SOURCE } = constants;
+
+loader.lazyRequireGetter(
+ this,
+ "getGripPreviewItems",
+ "resource://devtools/client/shared/components/reps/index.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "getUnicodeUrlPath",
+ "resource://devtools/client/shared/unicode-url.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "getSourceNames",
+ "resource://devtools/client/shared/source-utils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ [
+ "areMessagesSimilar",
+ "createWarningGroupMessage",
+ "isWarningGroup",
+ "getWarningGroupType",
+ "getDescriptorValue",
+ "getParentWarningGroupMessageId",
+ "getNaturalOrder",
+ ],
+ "resource://devtools/client/webconsole/utils/messages.js",
+ true
+);
+
+const {
+ UPDATE_REQUEST,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+
+const {
+ processNetworkUpdates,
+} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
+
+const MessageState = overrides =>
+ Object.freeze(
+ Object.assign(
+ {
+ // List of all the messages added to the console. Unlike other properties, this Map
+ // will be mutated on state changes for performance reasons.
+ mutableMessagesById: new Map(),
+ // Array of message ids, in chronological order. We use a dedicated property to store
+ // the order (instead of relying on the order of insertion in mutableMessagesById)
+ // as we might receive messages that need to be inserted at a specific index. Doing
+ // so on the Map can be costly, especially when the Map holds lots of messages.
+ mutableMessagesOrder: [],
+ // List of elements matching the selector of CSS Warning messages(populated
+ // on-demand via the UI).
+ cssMessagesMatchingElements: new Map(),
+ // Array of the visible messages.
+ visibleMessages: [],
+ // Object for the filtered messages.
+ filteredMessagesCount: getDefaultFiltersCounter(),
+ // List of the message ids which are opened.
+ messagesUiById: [],
+ // Map of the form {groupMessageId : groupArray},
+ // where groupArray is the list of of all the parent groups' ids of the groupMessageId.
+ // This handles console API groups.
+ groupsById: new Map(),
+ // Message id of the current console API group (no corresponding console.groupEnd yet).
+ currentGroup: null,
+ // This group handles "warning groups" (Content Blocking, CORS, CSP, …)
+ warningGroupsById: new Map(),
+ // Array of fronts to release (i.e. fronts logged in removed messages).
+ // This array *should not* be consumed by any UI component.
+ frontsToRelease: [],
+ // Map of the form {messageId : numberOfRepeat}
+ repeatById: {},
+ // Map of the form {messageId : networkInformation}
+ // `networkInformation` holds request, response, totalTime, ...
+ networkMessagesUpdateById: {},
+ // Id of the last messages that was added.
+ lastMessageId: null,
+ // List of the message ids which are disabled
+ disabledMessagesById: [],
+ },
+ overrides
+ )
+ );
+
+function cloneState(state) {
+ return {
+ visibleMessages: [...state.visibleMessages],
+ filteredMessagesCount: { ...state.filteredMessagesCount },
+ messagesUiById: [...state.messagesUiById],
+ cssMessagesMatchingElements: new Map(state.cssMessagesMatchingElements),
+ groupsById: new Map(state.groupsById),
+ frontsToRelease: [...state.frontsToRelease],
+ repeatById: { ...state.repeatById },
+ networkMessagesUpdateById: { ...state.networkMessagesUpdateById },
+ warningGroupsById: new Map(state.warningGroupsById),
+ // no need to mutate the properties below as they're not directly triggering re-render
+ mutableMessagesById: state.mutableMessagesById,
+ mutableMessagesOrder: state.mutableMessagesOrder,
+ currentGroup: state.currentGroup,
+ lastMessageId: state.lastMessageId,
+ disabledMessagesById: [...state.disabledMessagesById],
+ };
+}
+
+/**
+ * Add a console message to the state.
+ *
+ * @param {ConsoleMessage} newMessage: The message to add to the state.
+ * @param {MessageState} state: The message state ( = managed by this reducer).
+ * @param {FiltersState} filtersState: The filters state.
+ * @param {PrefsState} prefsState: The preferences state.
+ * @param {UiState} uiState: The ui state.
+ * @returns {MessageState} a new messages state.
+ */
+// eslint-disable-next-line complexity
+function addMessage(newMessage, state, filtersState, prefsState, uiState) {
+ const { mutableMessagesById, groupsById, repeatById } = state;
+
+ if (newMessage.type === constants.MESSAGE_TYPE.NAVIGATION_MARKER) {
+ // We set the state's currentGroup property to null after navigating
+ state.currentGroup = null;
+ }
+ const { currentGroup } = state;
+
+ if (newMessage.type === constants.MESSAGE_TYPE.NULL_MESSAGE) {
+ // When the message has a NULL type, we don't add it.
+ return state;
+ }
+
+ if (newMessage.type === constants.MESSAGE_TYPE.END_GROUP) {
+ // Compute the new current group.
+ state.currentGroup = getNewCurrentGroup(currentGroup, groupsById);
+ return state;
+ }
+
+ const lastMessage = mutableMessagesById.get(state.lastMessageId);
+ // It can happen that the new message was actually emitted earlier than the last message,
+ // which means we need to insert it at the right position.
+ const isUnsorted =
+ lastMessage && lastMessage.timeStamp > newMessage.timeStamp;
+
+ if (lastMessage && mutableMessagesById.size > 0) {
+ if (
+ lastMessage.groupId === currentGroup &&
+ areMessagesSimilar(lastMessage, newMessage)
+ ) {
+ state.repeatById[lastMessage.id] = (repeatById[lastMessage.id] || 1) + 1;
+ return state;
+ }
+ }
+
+ // Store the id of the message as being the last one being added.
+ if (!isUnsorted) {
+ state.lastMessageId = newMessage.id;
+ }
+
+ // Add the new message with a reference to the parent group.
+ const parentGroups = getParentGroups(currentGroup, groupsById);
+ if (!isWarningGroup(newMessage)) {
+ newMessage.groupId = currentGroup;
+ newMessage.indent = parentGroups.length;
+ }
+
+ // Check if the current message could be placed in a Warning Group.
+ // This needs to be done before setting the new message in mutableMessagesById so we have a
+ // proper message.
+ const warningGroupType = getWarningGroupType(newMessage);
+
+ // If the preference for warning grouping is true, and the new message could be in a
+ // warning group.
+ if (prefsState.groupWarnings && warningGroupType !== null) {
+ const warningGroupMessageId = getParentWarningGroupMessageId(newMessage);
+
+ // If there's no warning group for the type/innerWindowID yet
+ if (!state.mutableMessagesById.has(warningGroupMessageId)) {
+ // We create it and add it to the store.
+ const groupMessage = createWarningGroupMessage(
+ warningGroupMessageId,
+ warningGroupType,
+ newMessage
+ );
+ state = addMessage(
+ groupMessage,
+ state,
+ filtersState,
+ prefsState,
+ uiState
+ );
+ }
+
+ // We add the new message to the appropriate warningGroup.
+ state.warningGroupsById.get(warningGroupMessageId).push(newMessage.id);
+
+ // If the warningGroup message is not visible yet, but should be.
+ if (
+ !state.visibleMessages.includes(warningGroupMessageId) &&
+ getMessageVisibility(
+ state.mutableMessagesById.get(warningGroupMessageId),
+ {
+ messagesState: state,
+ filtersState,
+ prefsState,
+ uiState,
+ }
+ ).visible
+ ) {
+ // Then we put it in the visibleMessages properties, at the position of the first
+ // warning message inside the warningGroup.
+ // If that first warning message is in a console.group, we place it before the
+ // outermost console.group message.
+ const firstWarningMessageId = state.warningGroupsById.get(
+ warningGroupMessageId
+ )[0];
+ const firstWarningMessage = state.mutableMessagesById.get(
+ firstWarningMessageId
+ );
+ const outermostGroupId = getOutermostGroup(
+ firstWarningMessage,
+ groupsById
+ );
+ const groupIndex = state.visibleMessages.indexOf(outermostGroupId);
+ const warningMessageIndex = state.visibleMessages.indexOf(
+ firstWarningMessageId
+ );
+
+ if (groupIndex > -1) {
+ // We remove the warning message
+ if (warningMessageIndex > -1) {
+ state.visibleMessages.splice(warningMessageIndex, 1);
+ }
+
+ // And we put the warning group before the console.group
+ state.visibleMessages.splice(groupIndex, 0, warningGroupMessageId);
+ } else {
+ // If the warning message is not in a console.group, we replace it by the
+ // warning group message.
+ state.visibleMessages.splice(
+ warningMessageIndex,
+ 1,
+ warningGroupMessageId
+ );
+ }
+ }
+ }
+
+ // If we're creating a warningGroup, we init the array for its children.
+ if (isWarningGroup(newMessage)) {
+ state.warningGroupsById.set(newMessage.id, []);
+ }
+
+ const addedMessage = Object.freeze(newMessage);
+
+ // If the new message isn't the "oldest" one, then we need to insert it at the right
+ // position in the message map.
+ if (isUnsorted) {
+ let newMessageIndex = 0;
+ // This is can be on a hot path, so we're not using `findIndex`, which could be slow.
+ // Furthermore, there's a high chance the message beed to be inserted somewhere at the
+ // end of the list, so we loop through mutableMessagesOrder in reverse order.
+ for (let i = state.mutableMessagesOrder.length - 1; i >= 0; i--) {
+ const message = state.mutableMessagesById.get(
+ state.mutableMessagesOrder[i]
+ );
+ if (message.timeStamp <= addedMessage.timeStamp) {
+ newMessageIndex = i + 1;
+ break;
+ }
+ }
+
+ state.mutableMessagesOrder.splice(newMessageIndex, 0, addedMessage.id);
+ } else {
+ state.mutableMessagesOrder.push(addedMessage.id);
+ }
+ state.mutableMessagesById.set(addedMessage.id, addedMessage);
+
+ if (newMessage.type === "trace") {
+ // We want the stacktrace to be open by default.
+ state.messagesUiById.push(newMessage.id);
+ } else if (isGroupType(newMessage.type)) {
+ state.currentGroup = newMessage.id;
+ state.groupsById.set(newMessage.id, parentGroups);
+
+ if (newMessage.type === constants.MESSAGE_TYPE.START_GROUP) {
+ // We want the group to be open by default.
+ state.messagesUiById.push(newMessage.id);
+ }
+ }
+
+ const { visible, cause } = getMessageVisibility(addedMessage, {
+ messagesState: state,
+ filtersState,
+ prefsState,
+ uiState,
+ });
+
+ if (visible) {
+ // If the message is part of a visible warning group, we want to add it after the last
+ // visible message of the group.
+ const warningGroupId = getParentWarningGroupMessageId(newMessage);
+ if (warningGroupId && state.visibleMessages.includes(warningGroupId)) {
+ // Defaults to the warning group message.
+ let index = state.visibleMessages.indexOf(warningGroupId);
+
+ // We loop backward through the warning group's messages to get the latest visible
+ // messages in it.
+ const messagesInWarningGroup =
+ state.warningGroupsById.get(warningGroupId);
+ for (let i = messagesInWarningGroup.length - 1; i >= 0; i--) {
+ const idx = state.visibleMessages.indexOf(messagesInWarningGroup[i]);
+ if (idx > -1) {
+ index = idx;
+ break;
+ }
+ }
+ // Inserts the new warning message at the wanted location "in" the warning group.
+ state.visibleMessages.splice(index + 1, 0, newMessage.id);
+ } else if (isUnsorted) {
+ // If the new message wasn't the "oldest" one, then we need to insert its id at
+ // the right position in the array.
+ // This is can be on a hot path, so we're not using `findIndex`, which could be slow.
+ // Furthermore, there's a high chance the message beed to be inserted somewhere at the
+ // end of the list, so we loop through visibleMessages in reverse order.
+ let index = 0;
+ for (let i = state.visibleMessages.length - 1; i >= 0; i--) {
+ const id = state.visibleMessages[i];
+ if (
+ state.mutableMessagesById.get(id).timeStamp <= newMessage.timeStamp
+ ) {
+ index = i + 1;
+ break;
+ }
+ }
+ state.visibleMessages.splice(index, 0, newMessage.id);
+ } else {
+ state.visibleMessages.push(newMessage.id);
+ }
+ maybeSortVisibleMessages(state, false);
+ } else if (DEFAULT_FILTERS.includes(cause)) {
+ state.filteredMessagesCount.global++;
+ state.filteredMessagesCount[cause]++;
+ }
+
+ // Append received network-data also into networkMessagesUpdateById
+ // that is responsible for collecting (lazy loaded) HTTP payload data.
+ if (newMessage.source == "network") {
+ state.networkMessagesUpdateById[newMessage.actor] = newMessage;
+ }
+
+ return state;
+}
+
+// eslint-disable-next-line complexity
+function messages(
+ state = MessageState(),
+ action,
+ filtersState,
+ prefsState,
+ uiState
+) {
+ const {
+ mutableMessagesById,
+ cssMessagesMatchingElements,
+ messagesUiById,
+ networkMessagesUpdateById,
+ groupsById,
+ visibleMessages,
+ disabledMessagesById,
+ } = state;
+
+ const { logLimit } = prefsState;
+
+ let newState;
+ switch (action.type) {
+ case constants.MESSAGES_ADD:
+ // If the action holds more messages than the log limit, we can preemptively remove
+ // messages that will never be rendered.
+ const batchHasMoreMessagesThanLogLimit =
+ action.messages.length > logLimit;
+ const list = batchHasMoreMessagesThanLogLimit ? [] : action.messages;
+ if (batchHasMoreMessagesThanLogLimit) {
+ let prunableCount = 0;
+ let lastMessage = null;
+ for (let i = action.messages.length - 1; i >= 0; i--) {
+ const message = action.messages[i];
+ if (
+ !message.groupId &&
+ !isGroupType(message.type) &&
+ message.type !== MESSAGE_TYPE.END_GROUP
+ ) {
+ const messagesSimilar = areMessagesSimilar(lastMessage, message);
+ if (!messagesSimilar) {
+ prunableCount++;
+ }
+ // Once we've added the max number of messages that can be added, stop.
+ // Except for repeated messages, where we keep adding over the limit.
+ if (prunableCount <= logLimit || messagesSimilar) {
+ list.unshift(action.messages[i]);
+ } else {
+ break;
+ }
+ } else {
+ list.unshift(message);
+ }
+ lastMessage = message;
+ }
+ }
+
+ newState = cloneState(state);
+ for (const message of list) {
+ newState = addMessage(
+ message,
+ newState,
+ filtersState,
+ prefsState,
+ uiState
+ );
+ }
+
+ return limitTopLevelMessageCount(newState, logLimit);
+
+ case constants.MESSAGES_CLEAR:
+ const frontsToRelease = [];
+ for (const message of state.mutableMessagesById.values()) {
+ // We want to minimize time spent in reducer as much as we can, so we're using
+ // prototype.push.apply here as it seems faster than other solutions (e.g. the
+ // spread operator, Array#concat, …)
+ Array.prototype.push.apply(
+ frontsToRelease,
+ getAllFrontsInMessage(message)
+ );
+ }
+ return MessageState({
+ // Store all actors from removed messages. This array is used by
+ // `releaseActorsEnhancer` to release all of those backend actors.
+ frontsToRelease,
+ });
+
+ case constants.PRIVATE_MESSAGES_CLEAR: {
+ const removedIds = new Set();
+ for (const [id, message] of mutableMessagesById) {
+ if (message.private === true) {
+ removedIds.add(id);
+ }
+ }
+
+ // If there's no private messages, there's no need to change the state.
+ if (removedIds.size === 0) {
+ return state;
+ }
+
+ return removeMessagesFromState(
+ {
+ ...state,
+ },
+ removedIds
+ );
+ }
+
+ case constants.TARGET_MESSAGES_REMOVE: {
+ const removedIds = new Set();
+ for (const [id, message] of mutableMessagesById) {
+ // Remove message from the target but not evaluations and their results, so
+ // 1. we're consistent with the filtering behavior, i.e. we never hide those
+ // 2. when switching mode from multiprocess to parent process and back to multi,
+ // if we'd clear evaluations we wouldn't have a way to get them back, unlike
+ // log messages and errors, which are still available in the server caches).
+ if (
+ message.targetFront == action.targetFront &&
+ message.type !== MESSAGE_TYPE.COMMAND &&
+ message.type !== MESSAGE_TYPE.RESULT
+ ) {
+ removedIds.add(id);
+ }
+ }
+
+ return removeMessagesFromState(
+ {
+ ...state,
+ },
+ removedIds
+ );
+ }
+
+ case constants.MESSAGES_DISABLE:
+ return {
+ ...state,
+ disabledMessagesById: [...disabledMessagesById, ...action.ids],
+ };
+
+ case constants.MESSAGE_OPEN:
+ const openState = { ...state };
+ openState.messagesUiById = [...messagesUiById, action.id];
+ const currMessage = mutableMessagesById.get(action.id);
+
+ // If the message is a console.group/groupCollapsed or a warning group.
+ if (isGroupType(currMessage.type) || isWarningGroup(currMessage)) {
+ // We want to make its children visible
+ const messagesToShow = [];
+ for (const id of state.mutableMessagesOrder) {
+ const message = mutableMessagesById.get(id);
+ if (
+ !visibleMessages.includes(message.id) &&
+ ((isWarningGroup(currMessage) && !!getWarningGroupType(message)) ||
+ (isGroupType(currMessage.type) &&
+ getParentGroups(message.groupId, groupsById).includes(
+ action.id
+ ))) &&
+ getMessageVisibility(message, {
+ messagesState: openState,
+ filtersState,
+ prefsState,
+ uiState,
+ // We want to check if the message is in an open group
+ // only if it is not a direct child of the group we're opening.
+ checkGroup: message.groupId !== action.id,
+ }).visible
+ ) {
+ messagesToShow.push(id);
+ }
+ }
+
+ // We can then insert the messages ids right after the one of the group.
+ const insertIndex = visibleMessages.indexOf(action.id) + 1;
+ openState.visibleMessages = [
+ ...visibleMessages.slice(0, insertIndex),
+ ...messagesToShow,
+ ...visibleMessages.slice(insertIndex),
+ ];
+ }
+ return openState;
+
+ case constants.MESSAGE_CLOSE:
+ const closeState = { ...state };
+ const messageId = action.id;
+ const index = closeState.messagesUiById.indexOf(messageId);
+ closeState.messagesUiById.splice(index, 1);
+ closeState.messagesUiById = [...closeState.messagesUiById];
+
+ // If the message is a group
+ if (isGroupType(mutableMessagesById.get(messageId).type)) {
+ // Hide all its children, unless they're in a warningGroup.
+ closeState.visibleMessages = visibleMessages.filter((id, i, arr) => {
+ const message = mutableMessagesById.get(id);
+ const warningGroupMessage = mutableMessagesById.get(
+ getParentWarningGroupMessageId(message)
+ );
+
+ // If the message is in a warning group, then we return its current visibility.
+ if (
+ shouldGroupWarningMessages(
+ warningGroupMessage,
+ closeState,
+ prefsState
+ )
+ ) {
+ return arr.includes(id);
+ }
+
+ const parentGroups = getParentGroups(message.groupId, groupsById);
+ return parentGroups.includes(messageId) === false;
+ });
+ } else if (isWarningGroup(mutableMessagesById.get(messageId))) {
+ // If the message was a warningGroup, we hide all the messages in the group.
+ const groupMessages = closeState.warningGroupsById.get(messageId);
+ closeState.visibleMessages = visibleMessages.filter(
+ id => !groupMessages.includes(id)
+ );
+ }
+ return closeState;
+
+ case constants.CSS_MESSAGE_ADD_MATCHING_ELEMENTS:
+ return {
+ ...state,
+ cssMessagesMatchingElements: new Map(cssMessagesMatchingElements).set(
+ action.id,
+ action.elements
+ ),
+ };
+
+ case constants.NETWORK_MESSAGES_UPDATE:
+ const updatedState = {
+ ...state,
+ networkMessagesUpdateById: {
+ ...networkMessagesUpdateById,
+ },
+ };
+ let hasNetworkError = null;
+ for (const message of action.messages) {
+ const { id } = message;
+ updatedState.mutableMessagesById.set(id, message);
+ updatedState.networkMessagesUpdateById[id] = {
+ ...(updatedState.networkMessagesUpdateById[id] || {}),
+ ...message,
+ };
+
+ if (isMessageNetworkError(message)) {
+ hasNetworkError = true;
+ }
+ }
+
+ // If the message updates contained a network error, then we may have to display it.
+ if (hasNetworkError) {
+ return setVisibleMessages({
+ messagesState: updatedState,
+ filtersState,
+ prefsState,
+ uiState,
+ });
+ }
+
+ return updatedState;
+
+ case UPDATE_REQUEST:
+ case constants.NETWORK_UPDATES_REQUEST: {
+ newState = {
+ ...state,
+ networkMessagesUpdateById: {
+ ...networkMessagesUpdateById,
+ },
+ };
+
+ // Netmonitor's UPDATE_REQUEST action comes for only one request
+ const updates =
+ action.type == UPDATE_REQUEST
+ ? [{ id: action.id, data: action.data }]
+ : action.updates;
+ for (const { id, data } of updates) {
+ const request = newState.networkMessagesUpdateById[id];
+ if (!request) {
+ continue;
+ }
+ newState.networkMessagesUpdateById[id] = {
+ ...request,
+ ...processNetworkUpdates(data),
+ };
+ }
+ return newState;
+ }
+
+ case constants.FRONTS_TO_RELEASE_CLEAR:
+ return {
+ ...state,
+ frontsToRelease: [],
+ };
+
+ case constants.WARNING_GROUPS_TOGGLE:
+ // There's no warningGroups, and the pref was set to false,
+ // we don't need to do anything.
+ if (!prefsState.groupWarnings && state.warningGroupsById.size === 0) {
+ return state;
+ }
+
+ let needSort = false;
+ for (const msgId of state.mutableMessagesOrder) {
+ const message = state.mutableMessagesById.get(msgId);
+ const warningGroupType = getWarningGroupType(message);
+ if (warningGroupType) {
+ const warningGroupMessageId = getParentWarningGroupMessageId(message);
+
+ // If there's no warning group for the type/innerWindowID yet.
+ if (!state.mutableMessagesById.has(warningGroupMessageId)) {
+ // We create it and add it to the store.
+ const groupMessage = createWarningGroupMessage(
+ warningGroupMessageId,
+ warningGroupType,
+ message
+ );
+ state = addMessage(
+ groupMessage,
+ state,
+ filtersState,
+ prefsState,
+ uiState
+ );
+ }
+
+ // We add the new message to the appropriate warningGroup.
+ const warningGroup = state.warningGroupsById.get(
+ warningGroupMessageId
+ );
+ if (warningGroup && !warningGroup.includes(msgId)) {
+ warningGroup.push(msgId);
+ }
+
+ needSort = true;
+ }
+ }
+
+ // If we don't have any warning messages that could be in a group, we don't do
+ // anything.
+ if (!needSort) {
+ return state;
+ }
+
+ return setVisibleMessages({
+ messagesState: state,
+ filtersState,
+ prefsState,
+ uiState,
+ // If the user disabled warning groups, we want the messages to be sorted by their
+ // timestamps.
+ forceTimestampSort: !prefsState.groupWarnings,
+ });
+
+ case constants.MESSAGE_REMOVE:
+ return removeMessagesFromState(
+ {
+ ...state,
+ },
+ new Set([action.id])
+ );
+
+ case constants.FILTER_TOGGLE:
+ case constants.FILTER_TEXT_SET:
+ case constants.FILTERS_CLEAR:
+ case constants.DEFAULT_FILTERS_RESET:
+ return setVisibleMessages({
+ messagesState: state,
+ filtersState,
+ prefsState,
+ uiState,
+ });
+ }
+
+ return state;
+}
+
+function setVisibleMessages({
+ messagesState,
+ filtersState,
+ prefsState,
+ uiState,
+ forceTimestampSort = false,
+}) {
+ const {
+ mutableMessagesById,
+ mutableMessagesOrder,
+ visibleMessages,
+ messagesUiById,
+ } = messagesState;
+
+ const messagesToShow = new Set();
+ const matchedGroups = new Set();
+ const filtered = getDefaultFiltersCounter();
+
+ mutableMessagesOrder.forEach(msgId => {
+ const message = mutableMessagesById.get(msgId);
+ const groupParentId = message.groupId;
+ let hasMatchedAncestor = false;
+ const ancestors = [];
+
+ if (groupParentId) {
+ let ancestorId = groupParentId;
+
+ // we track the message's ancestors and their state
+ while (ancestorId) {
+ ancestors.push({
+ ancestorId,
+ matchedFilters: matchedGroups.has(ancestorId),
+ isOpen: messagesUiById.includes(ancestorId),
+ isCurrentlyVisible: visibleMessages.includes(ancestorId),
+ });
+ if (!hasMatchedAncestor && matchedGroups.has(ancestorId)) {
+ hasMatchedAncestor = true;
+ }
+ ancestorId = mutableMessagesById.get(ancestorId).groupId;
+ }
+ }
+
+ const { visible, cause } = getMessageVisibility(message, {
+ messagesState,
+ filtersState,
+ prefsState,
+ uiState,
+ hasMatchedAncestor,
+ });
+
+ // if the message is not visible but passes the search filters, we show its visible ancestors
+ if (!visible && passSearchFilters(message, filtersState)) {
+ const tmp = [];
+ ancestors.forEach(msg => {
+ if (msg.isCurrentlyVisible) {
+ tmp.push(msg.ancestorId);
+ }
+ });
+ tmp.reverse().forEach(id => {
+ messagesToShow.add(id);
+ });
+ }
+ if (visible) {
+ // if the visible message is a child of a group, we add its ancestors to the visible messages
+ if (groupParentId) {
+ // We need to reverse the visibleAncestors array to show the groups in the correct order
+ ancestors.reverse().forEach(msg => {
+ messagesToShow.add(msg.ancestorId);
+ });
+ }
+
+ // we keep track of matched startGroup and startGroupCollapsed messages so we don't filter their children
+ if (
+ message.type === "startGroup" ||
+ message.type === "startGroupCollapsed"
+ ) {
+ matchedGroups.add(msgId);
+ }
+
+ messagesToShow.add(msgId);
+ } else if (DEFAULT_FILTERS.includes(cause)) {
+ filtered.global = filtered.global + 1;
+ filtered[cause] = filtered[cause] + 1;
+ }
+ });
+
+ const newState = {
+ ...messagesState,
+ visibleMessages: Array.from(messagesToShow),
+ filteredMessagesCount: filtered,
+ };
+
+ maybeSortVisibleMessages(
+ newState,
+ // Only sort for warningGroups if the feature is enabled
+ prefsState.groupWarnings,
+ forceTimestampSort
+ );
+
+ return newState;
+}
+
+/**
+ * Returns the new current group id given the previous current group and the groupsById
+ * state property.
+ *
+ * @param {String} currentGroup: id of the current group
+ * @param {Map} groupsById
+ * @param {Array} ignoredIds: An array of ids which can't be the new current group.
+ * @returns {String|null} The new current group id, or null if there isn't one.
+ */
+function getNewCurrentGroup(currentGroup, groupsById, ignoredIds = new Set()) {
+ if (!currentGroup) {
+ return null;
+ }
+
+ // Retrieve the parent groups of the current group.
+ const parents = groupsById.get(currentGroup);
+
+ // If there's at least one parent, make the first one the new currentGroup.
+ if (Array.isArray(parents) && parents.length) {
+ // If the found group must be ignored, let's search for its parent.
+ if (ignoredIds.has(parents[0])) {
+ return getNewCurrentGroup(parents[0], groupsById, ignoredIds);
+ }
+
+ return parents[0];
+ }
+
+ return null;
+}
+
+function getParentGroups(currentGroup, groupsById) {
+ let groups = [];
+ if (currentGroup) {
+ // If there is a current group, we add it as a parent
+ groups = [currentGroup];
+
+ // As well as all its parents, if it has some.
+ const parentGroups = groupsById.get(currentGroup);
+ if (Array.isArray(parentGroups) && parentGroups.length) {
+ groups = groups.concat(parentGroups);
+ }
+ }
+
+ return groups;
+}
+
+function getOutermostGroup(message, groupsById) {
+ const groups = getParentGroups(message.groupId, groupsById);
+ if (groups.length === 0) {
+ return null;
+ }
+ return groups[groups.length - 1];
+}
+
+/**
+ * Remove all top level messages that exceeds message limit.
+ * Also populate an array of all backend actors associated with these
+ * messages so they can be released.
+ */
+function limitTopLevelMessageCount(newState, logLimit) {
+ let topLevelCount =
+ newState.groupsById.size === 0
+ ? newState.mutableMessagesById.size
+ : getToplevelMessageCount(newState);
+
+ if (topLevelCount <= logLimit) {
+ return newState;
+ }
+
+ const removedMessagesId = new Set();
+
+ let cleaningGroup = false;
+ for (const id of newState.mutableMessagesOrder) {
+ const message = newState.mutableMessagesById.get(id);
+ // If we were cleaning a group and the current message does not have
+ // a groupId, we're done cleaning.
+ if (cleaningGroup === true && !message.groupId) {
+ cleaningGroup = false;
+ }
+
+ // If we're not cleaning a group and the message count is below the logLimit,
+ // we exit the loop.
+ if (cleaningGroup === false && topLevelCount <= logLimit) {
+ break;
+ }
+
+ // If we're not currently cleaning a group, and the current message is identified
+ // as a group, set the cleaning flag to true.
+ if (cleaningGroup === false && newState.groupsById.has(id)) {
+ cleaningGroup = true;
+ }
+
+ if (!message.groupId) {
+ topLevelCount--;
+ }
+
+ removedMessagesId.add(id);
+ }
+
+ return removeMessagesFromState(newState, removedMessagesId);
+}
+
+/**
+ * Clean the properties for a given state object and an array of removed messages ids.
+ * Be aware that this function MUTATE the `state` argument.
+ *
+ * @param {MessageState} state
+ * @param {Set} removedMessagesIds
+ * @returns {MessageState}
+ */
+function removeMessagesFromState(state, removedMessagesIds) {
+ if (removedMessagesIds.size === 0) {
+ return state;
+ }
+
+ const frontsToRelease = [];
+ const visibleMessages = [...state.visibleMessages];
+ removedMessagesIds.forEach(id => {
+ const index = visibleMessages.indexOf(id);
+ if (index > -1) {
+ visibleMessages.splice(index, 1);
+ }
+
+ // We want to minimize time spent in reducer as much as we can, so we're using
+ // prototype.push.apply here as it seems faster than other solutions (e.g. the
+ // spread operator, Array#concat, …)
+ Array.prototype.push.apply(
+ frontsToRelease,
+ getAllFrontsInMessage(state.mutableMessagesById.get(id))
+ );
+ });
+
+ if (state.visibleMessages.length > visibleMessages.length) {
+ state.visibleMessages = visibleMessages;
+ }
+
+ if (frontsToRelease.length) {
+ state.frontsToRelease = state.frontsToRelease.concat(frontsToRelease);
+ }
+
+ const isInRemovedId = id => removedMessagesIds.has(id);
+ const mapHasRemovedIdKey = map => {
+ for (const id of removedMessagesIds) {
+ if (map.has(id)) {
+ return true;
+ }
+ }
+ return false;
+ };
+ const objectHasRemovedIdKey = obj =>
+ Object.keys(obj).findIndex(isInRemovedId) !== -1;
+
+ const cleanUpMap = map => {
+ const clonedMap = new Map(map);
+ removedMessagesIds.forEach(id => clonedMap.delete(id));
+ return clonedMap;
+ };
+ const cleanUpObject = object =>
+ [...Object.entries(object)].reduce((res, [id, value]) => {
+ if (!isInRemovedId(id)) {
+ res[id] = value;
+ }
+ return res;
+ }, {});
+
+ removedMessagesIds.forEach(id => {
+ state.mutableMessagesById.delete(id);
+
+ state.mutableMessagesOrder.splice(
+ state.mutableMessagesOrder.indexOf(id),
+ 1
+ );
+ });
+
+ if (state.disabledMessagesById.find(isInRemovedId)) {
+ state.disabledMessagesById = state.disabledMessagesById.filter(
+ id => !isInRemovedId(id)
+ );
+ }
+
+ if (state.messagesUiById.find(isInRemovedId)) {
+ state.messagesUiById = state.messagesUiById.filter(
+ id => !isInRemovedId(id)
+ );
+ }
+
+ if (isInRemovedId(state.currentGroup)) {
+ state.currentGroup = getNewCurrentGroup(
+ state.currentGroup,
+ state.groupsById,
+ removedMessagesIds
+ );
+ }
+
+ if (mapHasRemovedIdKey(state.cssMessagesMatchingElements)) {
+ state.cssMessagesMatchingElements = cleanUpMap(
+ state.cssMessagesMatchingElements
+ );
+ }
+ if (mapHasRemovedIdKey(state.groupsById)) {
+ state.groupsById = cleanUpMap(state.groupsById);
+ }
+
+ if (objectHasRemovedIdKey(state.repeatById)) {
+ state.repeatById = cleanUpObject(state.repeatById);
+ }
+
+ if (objectHasRemovedIdKey(state.networkMessagesUpdateById)) {
+ state.networkMessagesUpdateById = cleanUpObject(
+ state.networkMessagesUpdateById
+ );
+ }
+
+ return state;
+}
+
+/**
+ * Get an array of all the fronts logged in a specific message.
+ *
+ * @param {Message} message: The message to get actors from.
+ * @return {Array<ObjectFront|LongStringFront>} An array containing all the fronts logged
+ * in a message.
+ */
+function getAllFrontsInMessage(message) {
+ const { parameters, messageText } = message;
+
+ const fronts = [];
+ const isFront = p => p && typeof p.release === "function";
+
+ if (Array.isArray(parameters)) {
+ message.parameters.forEach(parameter => {
+ if (isFront(parameter)) {
+ fronts.push(parameter);
+ }
+ });
+ }
+
+ if (isFront(messageText)) {
+ fronts.push(messageText);
+ }
+
+ return fronts;
+}
+
+/**
+ * Returns total count of top level messages (those which are not
+ * within a group).
+ */
+function getToplevelMessageCount(state) {
+ let count = 0;
+ state.mutableMessagesById.forEach(message => {
+ if (!message.groupId) {
+ count++;
+ }
+ });
+ return count;
+}
+
+/**
+ * Check if a message should be visible in the console output, and if not, what
+ * causes it to be hidden.
+ * @param {Message} message: The message to check
+ * @param {Object} option: An option object of the following shape:
+ * - {MessageState} messagesState: The current messages state
+ * - {FilterState} filtersState: The current filters state
+ * - {PrefsState} prefsState: The current preferences state
+ * - {UiState} uiState: The current ui state
+ * - {Boolean} checkGroup: Set to false to not check if a message should
+ * be visible because it is in a console.group.
+ * - {Boolean} checkParentWarningGroupVisibility: Set to false to not
+ * check if a message should be visible because it is in a
+ * warningGroup and the warningGroup is visible.
+ * - {Boolean} hasMatchedAncestor: Set to true if message is part of a
+ * group that has been set to visible
+ *
+ * @return {Object} An object of the following form:
+ * - visible {Boolean}: true if the message should be visible
+ * - cause {String}: if visible is false, what causes the message to be hidden.
+ */
+// eslint-disable-next-line complexity
+function getMessageVisibility(
+ message,
+ {
+ messagesState,
+ filtersState,
+ prefsState,
+ uiState,
+ checkGroup = true,
+ checkParentWarningGroupVisibility = true,
+ hasMatchedAncestor = false,
+ }
+) {
+ const warningGroupMessageId = getParentWarningGroupMessageId(message);
+ const parentWarningGroupMessage = messagesState.mutableMessagesById.get(
+ warningGroupMessageId
+ );
+
+ // Do not display the message if it's in closed group and not in a warning group.
+ if (
+ checkGroup &&
+ !isInOpenedGroup(
+ message,
+ messagesState.groupsById,
+ messagesState.messagesUiById
+ ) &&
+ !shouldGroupWarningMessages(
+ parentWarningGroupMessage,
+ messagesState,
+ prefsState
+ )
+ ) {
+ return {
+ visible: false,
+ cause: "closedGroup",
+ };
+ }
+
+ // If the message is a warningGroup, check if it should be displayed.
+ if (isWarningGroup(message)) {
+ if (!shouldGroupWarningMessages(message, messagesState, prefsState)) {
+ return {
+ visible: false,
+ cause: "warningGroupHeuristicNotMet",
+ };
+ }
+
+ // Hide a warningGroup if the warning filter is off.
+ if (!filtersState[FILTERS.WARN]) {
+ // We don't include any cause as we don't want that message to be reflected in the
+ // message count.
+ return {
+ visible: false,
+ };
+ }
+
+ // Display a warningGroup if at least one of its message will be visible.
+ const childrenMessages = messagesState.warningGroupsById.get(message.id);
+ const hasVisibleChild =
+ childrenMessages &&
+ childrenMessages.some(id => {
+ const child = messagesState.mutableMessagesById.get(id);
+ if (!child) {
+ return false;
+ }
+
+ const { visible, cause } = getMessageVisibility(child, {
+ messagesState,
+ filtersState,
+ prefsState,
+ uiState,
+ checkParentWarningGroupVisibility: false,
+ });
+ return visible && cause !== "visibleWarningGroup";
+ });
+
+ if (hasVisibleChild) {
+ return {
+ visible: true,
+ cause: "visibleChild",
+ };
+ }
+ }
+
+ // Do not display the message if it can be in a warningGroup, and the group is
+ // displayed but collapsed.
+ if (
+ parentWarningGroupMessage &&
+ shouldGroupWarningMessages(
+ parentWarningGroupMessage,
+ messagesState,
+ prefsState
+ ) &&
+ !messagesState.messagesUiById.includes(warningGroupMessageId)
+ ) {
+ return {
+ visible: false,
+ cause: "closedWarningGroup",
+ };
+ }
+
+ // Display a message if it is in a warningGroup that is visible. We don't check the
+ // warningGroup visibility if `checkParentWarningGroupVisibility` is false, because
+ // it means we're checking the warningGroup visibility based on the visibility of its
+ // children, which would cause an infinite loop.
+ const parentVisibility =
+ parentWarningGroupMessage && checkParentWarningGroupVisibility
+ ? getMessageVisibility(parentWarningGroupMessage, {
+ messagesState,
+ filtersState,
+ prefsState,
+ uiState,
+ checkGroup,
+ checkParentWarningGroupVisibility,
+ })
+ : null;
+ if (
+ parentVisibility &&
+ parentVisibility.visible &&
+ parentVisibility.cause !== "visibleChild"
+ ) {
+ return {
+ visible: true,
+ cause: "visibleWarningGroup",
+ };
+ }
+
+ // Some messages can't be filtered out (e.g. groups).
+ // So, always return visible: true for those.
+ if (isUnfilterable(message)) {
+ return {
+ visible: true,
+ };
+ }
+
+ // Let's check all level filters (error, warn, log, …) and return visible: false
+ // and the message level as a cause if the function returns false.
+ if (!passLevelFilters(message, filtersState)) {
+ return {
+ visible: false,
+ cause: message.level,
+ };
+ }
+
+ if (!passCssFilters(message, filtersState)) {
+ return {
+ visible: false,
+ cause: FILTERS.CSS,
+ };
+ }
+
+ if (!passNetworkFilter(message, filtersState)) {
+ return {
+ visible: false,
+ cause: FILTERS.NET,
+ };
+ }
+
+ if (!passXhrFilter(message, filtersState)) {
+ return {
+ visible: false,
+ cause: FILTERS.NETXHR,
+ };
+ }
+
+ // This should always be the last check, or we might report that a message was hidden
+ // because of text search, while it may be hidden because its category is disabled.
+ // Do not check for search filters if it is part of a group and one of its ancestor
+ // has matched the current search filters and set to visible
+ if (!hasMatchedAncestor && !passSearchFilters(message, filtersState)) {
+ return {
+ visible: false,
+ cause: FILTERS.TEXT,
+ };
+ }
+
+ return {
+ visible: true,
+ };
+}
+
+function isUnfilterable(message) {
+ return [
+ MESSAGE_TYPE.COMMAND,
+ MESSAGE_TYPE.RESULT,
+ MESSAGE_TYPE.NAVIGATION_MARKER,
+ ].includes(message.type);
+}
+
+function isInOpenedGroup(message, groupsById, messagesUI) {
+ return (
+ !message.groupId ||
+ (!isGroupClosed(message.groupId, messagesUI) &&
+ !hasClosedParentGroup(groupsById.get(message.groupId), messagesUI))
+ );
+}
+
+function hasClosedParentGroup(group, messagesUI) {
+ return group.some(groupId => isGroupClosed(groupId, messagesUI));
+}
+
+function isGroupClosed(groupId, messagesUI) {
+ return messagesUI.includes(groupId) === false;
+}
+
+/**
+ * Returns true if the message shouldn't be hidden because of the network filter state.
+ *
+ * @param {Object} message - The message to check the filter against.
+ * @param {FilterState} filters - redux "filters" state.
+ * @returns {Boolean}
+ */
+function passNetworkFilter(message, filters) {
+ // The message passes the filter if it is not a network message,
+ // or if it is an xhr one,
+ // or if the network filter is on.
+ return (
+ message.source !== MESSAGE_SOURCE.NETWORK ||
+ message.isXHR === true ||
+ filters[FILTERS.NET] === true ||
+ (filters[FILTERS.ERROR] && isMessageNetworkError(message))
+ );
+}
+
+/**
+ * Returns true if the message shouldn't be hidden because of the xhr filter state.
+ *
+ * @param {Object} message - The message to check the filter against.
+ * @param {FilterState} filters - redux "filters" state.
+ * @returns {Boolean}
+ */
+function passXhrFilter(message, filters) {
+ // The message passes the filter if it is not a network message,
+ // or if it is a non-xhr one,
+ // or if the xhr filter is on.
+ return (
+ message.source !== MESSAGE_SOURCE.NETWORK ||
+ message.isXHR === false ||
+ filters[FILTERS.NETXHR] === true ||
+ (filters[FILTERS.ERROR] && isMessageNetworkError(message))
+ );
+}
+
+/**
+ * Returns true if the message shouldn't be hidden because of levels filter state.
+ *
+ * @param {Object} message - The message to check the filter against.
+ * @param {FilterState} filters - redux "filters" state.
+ * @returns {Boolean}
+ */
+function passLevelFilters(message, filters) {
+ // The message passes the filter if it is not a console call,
+ // or if its level matches the state of the corresponding filter.
+ return (
+ (message.source !== MESSAGE_SOURCE.CONSOLE_API &&
+ message.source !== MESSAGE_SOURCE.JAVASCRIPT) ||
+ filters[message.level] === true ||
+ (filters[FILTERS.ERROR] && isMessageNetworkError(message))
+ );
+}
+
+/**
+ * Returns true if the message shouldn't be hidden because of the CSS filter state.
+ *
+ * @param {Object} message - The message to check the filter against.
+ * @param {FilterState} filters - redux "filters" state.
+ * @returns {Boolean}
+ */
+function passCssFilters(message, filters) {
+ // The message passes the filter if it is not a CSS message,
+ // or if the CSS filter is on.
+ return message.source !== MESSAGE_SOURCE.CSS || filters.css === true;
+}
+
+/**
+ * Returns true if the message shouldn't be hidden because of search filter state.
+ *
+ * @param {Object} message - The message to check the filter against.
+ * @param {FilterState} filters - redux "filters" state.
+ * @returns {Boolean}
+ */
+function passSearchFilters(message, filters) {
+ const trimmed = (filters.text || "").trim();
+
+ // "-"-prefix switched to exclude mode
+ const exclude = trimmed.startsWith("-");
+ const term = exclude ? trimmed.slice(1) : trimmed;
+
+ // This regex matches a very basic regex with an optional i flag
+ const regexMatch = /^\/(?<search>.+)\/(?<flags>i)?$/.exec(term);
+ let regex;
+ if (regexMatch !== null) {
+ const flags = "m" + (regexMatch.groups.flags || "");
+ try {
+ regex = new RegExp(regexMatch.groups.search, flags);
+ } catch (e) {}
+ }
+ const matchStr = regex
+ ? str => regex.test(str)
+ : str => str.toLocaleLowerCase().includes(term.toLocaleLowerCase());
+
+ // If there is no search, the message passes the filter.
+ if (!term) {
+ return true;
+ }
+
+ const matched =
+ // Look for a match in parameters.
+ isTextInParameters(matchStr, message.parameters) ||
+ // Look for a match in location.
+ isTextInFrame(matchStr, message.frame) ||
+ // Look for a match in net events.
+ isTextInNetEvent(matchStr, message) ||
+ // Look for a match in stack-trace.
+ isTextInStackTrace(matchStr, message.stacktrace) ||
+ // Look for a match in messageText.
+ isTextInMessageText(matchStr, message.messageText) ||
+ // Look for a match in notes.
+ isTextInNotes(matchStr, message.notes) ||
+ // Look for a match in prefix.
+ isTextInPrefix(matchStr, message.prefix) ||
+ // Look for a match in displayName.
+ isTextInDisplayName(matchStr, message.displayName);
+
+ return matched ? !exclude : exclude;
+}
+
+/**
+ * Returns true if given text is included in provided stack frame.
+ */
+function isTextInFrame(matchStr, frame) {
+ if (!frame) {
+ return false;
+ }
+
+ const { functionName, line, column, source } = frame;
+ const { short } = getSourceNames(source);
+ const unicodeShort = getUnicodeUrlPath(short);
+
+ const str = `${
+ functionName ? functionName + " " : ""
+ }${unicodeShort}:${line}:${column}`;
+ return matchStr(str);
+}
+
+/**
+ * Returns true if given text is included in provided parameters.
+ */
+function isTextInParameters(matchStr, parameters) {
+ if (!parameters) {
+ return false;
+ }
+
+ return parameters.some(parameter => isTextInParameter(matchStr, parameter));
+}
+
+/**
+ * Returns true if given text is included in provided parameter.
+ */
+function isTextInParameter(matchStr, parameter) {
+ const paramGrip =
+ parameter && parameter.getGrip ? parameter.getGrip() : parameter;
+
+ if (paramGrip && paramGrip.class && matchStr(paramGrip.class)) {
+ return true;
+ }
+
+ const parameterType = typeof parameter;
+ if (parameterType !== "object" && parameterType !== "undefined") {
+ const str = paramGrip + "";
+ if (matchStr(str)) {
+ return true;
+ }
+ }
+
+ const previewItems = getGripPreviewItems(paramGrip);
+ for (const item of previewItems) {
+ if (isTextInParameter(matchStr, item)) {
+ return true;
+ }
+ }
+
+ if (paramGrip && paramGrip.ownProperties) {
+ for (const [key, desc] of Object.entries(paramGrip.ownProperties)) {
+ if (matchStr(key)) {
+ return true;
+ }
+
+ if (isTextInParameter(matchStr, getDescriptorValue(desc))) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Returns true if given text is included in provided net event grip.
+ */
+function isTextInNetEvent(matchStr, { method, url } = {}) {
+ if (!method && !url) {
+ return false;
+ }
+ return matchStr(method) || matchStr(url);
+}
+
+/**
+ * Returns true if given text is included in provided stack trace.
+ */
+function isTextInStackTrace(matchStr, stacktrace) {
+ if (!Array.isArray(stacktrace)) {
+ return false;
+ }
+
+ // isTextInFrame expect the properties of the frame object to be in the same
+ // order they are rendered in the Frame component.
+ return stacktrace.some(frame =>
+ isTextInFrame(matchStr, {
+ functionName:
+ frame.functionName || l10n.getStr("stacktrace.anonymousFunction"),
+ source: frame.filename,
+ lineNumber: frame.lineNumber,
+ columnNumber: frame.columnNumber,
+ })
+ );
+}
+
+/**
+ * Returns true if given text is included in `messageText` field.
+ */
+function isTextInMessageText(matchStr, messageText) {
+ if (!messageText) {
+ return false;
+ }
+
+ if (typeof messageText === "string") {
+ return matchStr(messageText);
+ }
+
+ const grip =
+ messageText && messageText.getGrip ? messageText.getGrip() : messageText;
+ if (grip && grip.type === "longString") {
+ return matchStr(grip.initial);
+ }
+
+ return true;
+}
+
+/**
+ * Returns true if given text is included in JS Trace display name.
+ */
+function isTextInDisplayName(matchStr, displayName) {
+ return displayName && matchStr(displayName);
+}
+
+/**
+ * Returns true if given text is included in notes.
+ */
+function isTextInNotes(matchStr, notes) {
+ if (!Array.isArray(notes)) {
+ return false;
+ }
+
+ return notes.some(
+ note =>
+ // Look for a match in location.
+ isTextInFrame(matchStr, note.frame) ||
+ // Look for a match in messageBody.
+ (note.messageBody && matchStr(note.messageBody))
+ );
+}
+
+/**
+ * Returns true if given text is included in prefix.
+ */
+function isTextInPrefix(matchStr, prefix) {
+ if (!prefix) {
+ return false;
+ }
+
+ return matchStr(`${prefix}: `);
+}
+
+function getDefaultFiltersCounter() {
+ const count = DEFAULT_FILTERS.reduce((res, filter) => {
+ res[filter] = 0;
+ return res;
+ }, {});
+ count.global = 0;
+ return count;
+}
+
+/**
+ * Sort state.visibleMessages if needed.
+ *
+ * @param {MessageState} state
+ * @param {Boolean} sortWarningGroupMessage: set to true to sort warningGroup
+ * messages. Default to false, as in some
+ * situations we already take care of putting
+ * the ids at the right position.
+ * @param {Boolean} timeStampSort: set to true to sort messages by their timestamps.
+ */
+function maybeSortVisibleMessages(
+ state,
+ sortWarningGroupMessage = false,
+ timeStampSort = false
+) {
+ if (state.warningGroupsById.size > 0 && sortWarningGroupMessage) {
+ state.visibleMessages.sort((a, b) => {
+ const messageA = state.mutableMessagesById.get(a);
+ const messageB = state.mutableMessagesById.get(b);
+
+ const warningGroupIdA = getParentWarningGroupMessageId(messageA);
+ const warningGroupIdB = getParentWarningGroupMessageId(messageB);
+
+ const warningGroupA = state.mutableMessagesById.get(warningGroupIdA);
+ const warningGroupB = state.mutableMessagesById.get(warningGroupIdB);
+
+ const aFirst = -1;
+ const bFirst = 1;
+
+ // If both messages are in a warningGroup, or if both are not in warningGroups.
+ if (
+ (warningGroupA && warningGroupB) ||
+ (!warningGroupA && !warningGroupB)
+ ) {
+ return getNaturalOrder(messageA, messageB);
+ }
+
+ // If `a` is in a warningGroup (and `b` isn't).
+ if (warningGroupA) {
+ // If `b` is the warningGroup of `a`, `a` should be after `b`.
+ if (warningGroupIdA === messageB.id) {
+ return bFirst;
+ }
+ // `b` is a regular message, we place `a` before `b` if `b` came after `a`'s
+ // warningGroup.
+ return getNaturalOrder(warningGroupA, messageB);
+ }
+
+ // If `b` is in a warningGroup (and `a` isn't).
+ if (warningGroupB) {
+ // If `a` is the warningGroup of `b`, `a` should be before `b`.
+ if (warningGroupIdB === messageA.id) {
+ return aFirst;
+ }
+ // `a` is a regular message, we place `a` after `b` if `a` came after `b`'s
+ // warningGroup.
+ return getNaturalOrder(messageA, warningGroupB);
+ }
+
+ return 0;
+ });
+ }
+
+ if (timeStampSort) {
+ state.visibleMessages.sort((a, b) => {
+ const messageA = state.mutableMessagesById.get(a);
+ const messageB = state.mutableMessagesById.get(b);
+ return getNaturalOrder(messageA, messageB);
+ });
+ }
+}
+
+/**
+ * Returns if a given type of warning message should be grouped.
+ *
+ * @param {ConsoleMessage} warningGroupMessage
+ * @param {MessageState} messagesState
+ * @param {PrefsState} prefsState
+ */
+function shouldGroupWarningMessages(
+ warningGroupMessage,
+ messagesState,
+ prefsState
+) {
+ if (!warningGroupMessage) {
+ return false;
+ }
+
+ // Only group if the preference is ON.
+ if (!prefsState.groupWarnings) {
+ return false;
+ }
+
+ // We group warning messages if there are at least 2 messages that could go in it.
+ const warningGroup = messagesState.warningGroupsById.get(
+ warningGroupMessage.id
+ );
+ if (!warningGroup || !Array.isArray(warningGroup)) {
+ return false;
+ }
+
+ return warningGroup.length > 1;
+}
+
+exports.messages = messages;
diff --git a/devtools/client/webconsole/reducers/moz.build b/devtools/client/webconsole/reducers/moz.build
new file mode 100644
index 0000000000..b6380ba3be
--- /dev/null
+++ b/devtools/client/webconsole/reducers/moz.build
@@ -0,0 +1,15 @@
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "autocomplete.js",
+ "filters.js",
+ "history.js",
+ "index.js",
+ "messages.js",
+ "notifications.js",
+ "prefs.js",
+ "ui.js",
+)
diff --git a/devtools/client/webconsole/reducers/notifications.js b/devtools/client/webconsole/reducers/notifications.js
new file mode 100644
index 0000000000..d3a2866986
--- /dev/null
+++ b/devtools/client/webconsole/reducers/notifications.js
@@ -0,0 +1,58 @@
+/* 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";
+
+const {
+ APPEND_NOTIFICATION,
+ REMOVE_NOTIFICATION,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+loader.lazyRequireGetter(
+ this,
+ ["appendNotification", "removeNotificationWithValue"],
+ "resource://devtools/client/shared/components/NotificationBox.js",
+ true
+);
+
+/**
+ * Create default initial state for this reducer. The state is composed
+ * from list of notifications.
+ */
+function getInitialState() {
+ return {
+ notifications: undefined,
+ };
+}
+
+/**
+ * Reducer function implementation. This reducers is responsible
+ * for maintaining list of notifications. It's consumed by
+ * `NotificationBox` component.
+ */
+function notifications(state = getInitialState(), action) {
+ switch (action.type) {
+ case APPEND_NOTIFICATION:
+ return append(state, action);
+ case REMOVE_NOTIFICATION:
+ return remove(state, action);
+ }
+
+ return state;
+}
+
+// Helpers
+
+function append(state, action) {
+ return appendNotification(state, action);
+}
+
+function remove(state, action) {
+ return removeNotificationWithValue(state.notifications, action.value);
+}
+
+// Exports
+
+module.exports = {
+ notifications,
+};
diff --git a/devtools/client/webconsole/reducers/prefs.js b/devtools/client/webconsole/reducers/prefs.js
new file mode 100644
index 0000000000..e19f5acd32
--- /dev/null
+++ b/devtools/client/webconsole/reducers/prefs.js
@@ -0,0 +1,48 @@
+/* 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";
+
+const {
+ EAGER_EVALUATION_TOGGLE,
+ WARNING_GROUPS_TOGGLE,
+ AUTOCOMPLETE_TOGGLE,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const PrefState = overrides =>
+ Object.freeze(
+ Object.assign(
+ {
+ logLimit: 1000,
+ sidebarToggle: false,
+ groupWarnings: false,
+ autocomplete: false,
+ eagerEvaluation: false,
+ historyCount: 50,
+ },
+ overrides
+ )
+ );
+
+const dict = {
+ [EAGER_EVALUATION_TOGGLE]: "eagerEvaluation",
+ [WARNING_GROUPS_TOGGLE]: "groupWarnings",
+ [AUTOCOMPLETE_TOGGLE]: "autocomplete",
+};
+
+function prefs(state = PrefState(), action) {
+ const pref = dict[action.type];
+ if (pref) {
+ return {
+ ...state,
+ [pref]: !state[pref],
+ };
+ }
+
+ return state;
+}
+
+module.exports = {
+ PrefState,
+ prefs,
+};
diff --git a/devtools/client/webconsole/reducers/ui.js b/devtools/client/webconsole/reducers/ui.js
new file mode 100644
index 0000000000..f3d8f0ca77
--- /dev/null
+++ b/devtools/client/webconsole/reducers/ui.js
@@ -0,0 +1,137 @@
+/* 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";
+
+const {
+ INITIALIZE,
+ MESSAGES_CLEAR,
+ PERSIST_TOGGLE,
+ REVERSE_SEARCH_INPUT_TOGGLE,
+ SELECT_NETWORK_MESSAGE_TAB,
+ SHOW_OBJECT_IN_SIDEBAR,
+ SIDEBAR_CLOSE,
+ SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE,
+ TIMESTAMPS_TOGGLE,
+ FILTERBAR_DISPLAY_MODE_SET,
+ FILTERBAR_DISPLAY_MODES,
+ EDITOR_ONBOARDING_DISMISS,
+ EDITOR_TOGGLE,
+ EDITOR_PRETTY_PRINT,
+ EDITOR_SET_WIDTH,
+ ENABLE_NETWORK_MONITORING,
+ SHOW_EVALUATION_NOTIFICATION,
+} = require("resource://devtools/client/webconsole/constants.js");
+
+const {
+ PANELS,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+
+const UiState = overrides =>
+ Object.freeze(
+ Object.assign(
+ {
+ initialized: false,
+ networkMessageActiveTabId: PANELS.HEADERS,
+ persistLogs: false,
+ sidebarVisible: false,
+ timestampsVisible: true,
+ frontInSidebar: null,
+ closeButtonVisible: false,
+ reverseSearchInputVisible: false,
+ reverseSearchInitialValue: "",
+ editor: false,
+ editorWidth: null,
+ editorPrettifiedAt: null,
+ showEditorOnboarding: false,
+ filterBarDisplayMode: FILTERBAR_DISPLAY_MODES.WIDE,
+ cacheGeneration: 0,
+ // Only used in the browser toolbox console/ browser console
+ // turned off by default
+ enableNetworkMonitoring: false,
+ notification: null,
+ },
+ overrides
+ )
+ );
+
+function ui(state = UiState(), action) {
+ switch (action.type) {
+ case PERSIST_TOGGLE:
+ return { ...state, persistLogs: !state.persistLogs };
+ case TIMESTAMPS_TOGGLE:
+ return { ...state, timestampsVisible: !state.timestampsVisible };
+ case SELECT_NETWORK_MESSAGE_TAB:
+ return { ...state, networkMessageActiveTabId: action.id };
+ case SIDEBAR_CLOSE:
+ return {
+ ...state,
+ sidebarVisible: false,
+ frontInSidebar: null,
+ };
+ case INITIALIZE:
+ return { ...state, initialized: true };
+ case MESSAGES_CLEAR:
+ return {
+ ...state,
+ sidebarVisible: false,
+ frontInSidebar: null,
+ cacheGeneration: state.cacheGeneration + 1,
+ };
+ case SHOW_OBJECT_IN_SIDEBAR:
+ if (action.front === state.frontInSidebar) {
+ return state;
+ }
+ return { ...state, sidebarVisible: true, frontInSidebar: action.front };
+ case SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE:
+ return { ...state, closeButtonVisible: action.shouldDisplayButton };
+ case SHOW_EVALUATION_NOTIFICATION:
+ if (state.notification == action.notification) {
+ return state;
+ }
+ return { ...state, notification: action.notification };
+ case REVERSE_SEARCH_INPUT_TOGGLE:
+ return {
+ ...state,
+ reverseSearchInputVisible: !state.reverseSearchInputVisible,
+ reverseSearchInitialValue: action.initialValue || "",
+ };
+ case FILTERBAR_DISPLAY_MODE_SET:
+ return {
+ ...state,
+ filterBarDisplayMode: action.displayMode,
+ };
+ case EDITOR_TOGGLE:
+ return {
+ ...state,
+ editor: !state.editor,
+ };
+ case EDITOR_ONBOARDING_DISMISS:
+ return {
+ ...state,
+ showEditorOnboarding: false,
+ };
+ case EDITOR_SET_WIDTH:
+ return {
+ ...state,
+ editorWidth: action.width,
+ };
+ case EDITOR_PRETTY_PRINT:
+ return {
+ ...state,
+ editorPrettifiedAt: Date.now(),
+ };
+ case ENABLE_NETWORK_MONITORING:
+ return {
+ ...state,
+ enableNetworkMonitoring: !state.enableNetworkMonitoring,
+ };
+ }
+
+ return state;
+}
+
+module.exports = {
+ UiState,
+ ui,
+};