mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 21:33:27 +00:00
Creates notification queue type standard. (#16009)
This commit is contained in:
@@ -4,11 +4,29 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { CollectionView } from "../../content/components/common-types";
|
||||
import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum";
|
||||
import { NotificationType, NotificationTypes } from "../../enums/notification-type.enum";
|
||||
import AutofillPageDetails from "../../models/autofill-page-details";
|
||||
|
||||
/**
|
||||
* @todo Remove Standard_ label when implemented as standard NotificationQueueMessage.
|
||||
*/
|
||||
export interface Standard_NotificationQueueMessage<T, D> {
|
||||
// universal notification properties
|
||||
domain: string;
|
||||
tab: chrome.tabs.Tab;
|
||||
launchTimestamp: number;
|
||||
expires: Date;
|
||||
wasVaultLocked: boolean;
|
||||
|
||||
type: T; // NotificationType
|
||||
data: D; // notification-specific data
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo Deprecate in favor of Standard_NotificationQueueMessage.
|
||||
*/
|
||||
interface NotificationQueueMessage {
|
||||
type: NotificationQueueMessageTypes;
|
||||
type: NotificationTypes;
|
||||
domain: string;
|
||||
tab: chrome.tabs.Tab;
|
||||
launchTimestamp: number;
|
||||
@@ -16,11 +34,15 @@ interface NotificationQueueMessage {
|
||||
wasVaultLocked: boolean;
|
||||
}
|
||||
|
||||
interface AddChangePasswordQueueMessage extends NotificationQueueMessage {
|
||||
type: "change";
|
||||
type ChangePasswordNotificationData = {
|
||||
cipherId: CipherView["id"];
|
||||
newPassword: string;
|
||||
}
|
||||
};
|
||||
|
||||
type AddChangePasswordNotificationQueueMessage = Standard_NotificationQueueMessage<
|
||||
typeof NotificationType.ChangePassword,
|
||||
ChangePasswordNotificationData
|
||||
>;
|
||||
|
||||
interface AddLoginQueueMessage extends NotificationQueueMessage {
|
||||
type: "add";
|
||||
@@ -41,7 +63,7 @@ interface AtRiskPasswordQueueMessage extends NotificationQueueMessage {
|
||||
|
||||
type NotificationQueueMessageItem =
|
||||
| AddLoginQueueMessage
|
||||
| AddChangePasswordQueueMessage
|
||||
| AddChangePasswordNotificationQueueMessage
|
||||
| AddUnlockVaultQueueMessage
|
||||
| AtRiskPasswordQueueMessage;
|
||||
|
||||
@@ -72,6 +94,11 @@ type UnlockVaultMessageData = {
|
||||
skipNotification?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* @todo Extend generics to this type, see Standard_NotificationQueueMessage
|
||||
* - use new `data` types as generic
|
||||
* - eliminate optional status of properties as needed per Notification Type
|
||||
*/
|
||||
type NotificationBackgroundExtensionMessage = {
|
||||
[key: string]: any;
|
||||
command: string;
|
||||
@@ -126,7 +153,7 @@ type NotificationBackgroundExtensionMessageHandlers = {
|
||||
};
|
||||
|
||||
export {
|
||||
AddChangePasswordQueueMessage,
|
||||
AddChangePasswordNotificationQueueMessage,
|
||||
AddLoginQueueMessage,
|
||||
AddUnlockVaultQueueMessage,
|
||||
NotificationQueueMessageItem,
|
||||
|
||||
@@ -26,14 +26,14 @@ import { FolderService } from "@bitwarden/common/vault/services/folder/folder.se
|
||||
import { TaskService, SecurityTask } from "@bitwarden/common/vault/tasks";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum";
|
||||
import { NotificationType } from "../enums/notification-type.enum";
|
||||
import { FormData } from "../services/abstractions/autofill.service";
|
||||
import AutofillService from "../services/autofill.service";
|
||||
import { createAutofillPageDetailsMock, createChromeTabMock } from "../spec/autofill-mocks";
|
||||
import { flushPromises, sendMockExtensionMessage } from "../spec/testing-utils";
|
||||
|
||||
import {
|
||||
AddChangePasswordQueueMessage,
|
||||
AddChangePasswordNotificationQueueMessage,
|
||||
AddLoginQueueMessage,
|
||||
AddUnlockVaultQueueMessage,
|
||||
LockedVaultPendingNotificationsData,
|
||||
@@ -761,7 +761,7 @@ describe("NotificationBackground", () => {
|
||||
notificationBackground["notificationQueue"] = [
|
||||
mock<AddUnlockVaultQueueMessage>({
|
||||
tab,
|
||||
type: NotificationQueueMessageType.UnlockVault,
|
||||
type: NotificationType.UnlockVault,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -783,7 +783,7 @@ describe("NotificationBackground", () => {
|
||||
};
|
||||
notificationBackground["notificationQueue"] = [
|
||||
mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "another.com",
|
||||
}),
|
||||
@@ -803,11 +803,11 @@ describe("NotificationBackground", () => {
|
||||
edit: false,
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddChangePasswordQueueMessage>({
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
const queueMessage = mock<AddChangePasswordNotificationQueueMessage>({
|
||||
type: NotificationType.ChangePassword,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
newPassword: "newPassword",
|
||||
data: { newPassword: "newPassword" },
|
||||
});
|
||||
notificationBackground["notificationQueue"] = [queueMessage];
|
||||
const cipherView = mock<CipherView>({
|
||||
@@ -825,7 +825,7 @@ describe("NotificationBackground", () => {
|
||||
expect(createWithServerSpy).not.toHaveBeenCalled();
|
||||
expect(updatePasswordSpy).toHaveBeenCalledWith(
|
||||
cipherView,
|
||||
queueMessage.newPassword,
|
||||
queueMessage.data.newPassword,
|
||||
message.edit,
|
||||
sender.tab,
|
||||
"testId",
|
||||
@@ -851,11 +851,11 @@ describe("NotificationBackground", () => {
|
||||
edit: false,
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddChangePasswordQueueMessage>({
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
const queueMessage = mock<AddChangePasswordNotificationQueueMessage>({
|
||||
type: NotificationType.ChangePassword,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
newPassword: "newPassword",
|
||||
data: { newPassword: "newPassword" },
|
||||
});
|
||||
notificationBackground["notificationQueue"] = [queueMessage];
|
||||
const cipherView = mock<CipherView>({
|
||||
@@ -874,7 +874,7 @@ describe("NotificationBackground", () => {
|
||||
expect(createWithServerSpy).not.toHaveBeenCalled();
|
||||
expect(updatePasswordSpy).toHaveBeenCalledWith(
|
||||
cipherView,
|
||||
queueMessage.newPassword,
|
||||
queueMessage.data.newPassword,
|
||||
message.edit,
|
||||
sender.tab,
|
||||
"testId",
|
||||
@@ -931,11 +931,11 @@ describe("NotificationBackground", () => {
|
||||
edit: false,
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddChangePasswordQueueMessage>({
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
const queueMessage = mock<AddChangePasswordNotificationQueueMessage>({
|
||||
type: NotificationType.ChangePassword,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
newPassword: "newPassword",
|
||||
data: { newPassword: "newPassword" },
|
||||
});
|
||||
notificationBackground["notificationQueue"] = [queueMessage];
|
||||
const cipherView = mock<CipherView>({
|
||||
@@ -953,7 +953,7 @@ describe("NotificationBackground", () => {
|
||||
expect(createWithServerSpy).not.toHaveBeenCalled();
|
||||
expect(updatePasswordSpy).toHaveBeenCalledWith(
|
||||
cipherView,
|
||||
queueMessage.newPassword,
|
||||
queueMessage.data.newPassword,
|
||||
message.edit,
|
||||
sender.tab,
|
||||
mockCipherId,
|
||||
@@ -983,7 +983,7 @@ describe("NotificationBackground", () => {
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
username: "test",
|
||||
@@ -1018,11 +1018,11 @@ describe("NotificationBackground", () => {
|
||||
edit: true,
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddChangePasswordQueueMessage>({
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
const queueMessage = mock<AddChangePasswordNotificationQueueMessage>({
|
||||
type: NotificationType.ChangePassword,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
newPassword: "newPassword",
|
||||
data: { newPassword: "newPassword" },
|
||||
});
|
||||
notificationBackground["notificationQueue"] = [queueMessage];
|
||||
const cipherView = mock<CipherView>();
|
||||
@@ -1035,7 +1035,7 @@ describe("NotificationBackground", () => {
|
||||
|
||||
expect(updatePasswordSpy).toHaveBeenCalledWith(
|
||||
cipherView,
|
||||
queueMessage.newPassword,
|
||||
queueMessage.data.newPassword,
|
||||
message.edit,
|
||||
sender.tab,
|
||||
"testId",
|
||||
@@ -1070,7 +1070,7 @@ describe("NotificationBackground", () => {
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
username: "test",
|
||||
@@ -1109,7 +1109,7 @@ describe("NotificationBackground", () => {
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
username: "test",
|
||||
@@ -1162,7 +1162,7 @@ describe("NotificationBackground", () => {
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
username: "test",
|
||||
@@ -1213,11 +1213,11 @@ describe("NotificationBackground", () => {
|
||||
edit: false,
|
||||
folder: "folder-id",
|
||||
};
|
||||
const queueMessage = mock<AddChangePasswordQueueMessage>({
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
const queueMessage = mock<AddChangePasswordNotificationQueueMessage>({
|
||||
type: NotificationType.ChangePassword,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
newPassword: "newPassword",
|
||||
data: { newPassword: "newPassword" },
|
||||
});
|
||||
notificationBackground["notificationQueue"] = [queueMessage];
|
||||
const cipherView = mock<CipherView>({ reprompt: CipherRepromptType.None });
|
||||
@@ -1273,7 +1273,7 @@ describe("NotificationBackground", () => {
|
||||
const sender = mock<chrome.runtime.MessageSender>({ tab });
|
||||
const message: NotificationBackgroundExtensionMessage = { command: "bgNeverSave" };
|
||||
notificationBackground["notificationQueue"] = [
|
||||
mock<AddUnlockVaultQueueMessage>({ type: NotificationQueueMessageType.UnlockVault, tab }),
|
||||
mock<AddUnlockVaultQueueMessage>({ type: NotificationType.UnlockVault, tab }),
|
||||
];
|
||||
|
||||
sendMockExtensionMessage(message, sender);
|
||||
@@ -1289,7 +1289,7 @@ describe("NotificationBackground", () => {
|
||||
const sender = mock<chrome.runtime.MessageSender>({ tab: secondaryTab });
|
||||
notificationBackground["notificationQueue"] = [
|
||||
mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "another.com",
|
||||
}),
|
||||
@@ -1306,12 +1306,12 @@ describe("NotificationBackground", () => {
|
||||
const sender = mock<chrome.runtime.MessageSender>({ tab });
|
||||
const message: NotificationBackgroundExtensionMessage = { command: "bgNeverSave" };
|
||||
const firstNotification = mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab,
|
||||
domain: "example.com",
|
||||
});
|
||||
const secondNotification = mock<AddLoginQueueMessage>({
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
tab: createChromeTabMock({ id: 3 }),
|
||||
domain: "another.com",
|
||||
});
|
||||
|
||||
@@ -60,12 +60,12 @@ import {
|
||||
NotificationCipherData,
|
||||
} from "../content/components/cipher/types";
|
||||
import { CollectionView } from "../content/components/common-types";
|
||||
import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum";
|
||||
import { NotificationType } from "../enums/notification-type.enum";
|
||||
import { AutofillService } from "../services/abstractions/autofill.service";
|
||||
import { TemporaryNotificationChangeLoginService } from "../services/notification-change-login-password.service";
|
||||
|
||||
import {
|
||||
AddChangePasswordQueueMessage,
|
||||
AddChangePasswordNotificationQueueMessage,
|
||||
AddLoginQueueMessage,
|
||||
AddUnlockVaultQueueMessage,
|
||||
AddLoginMessageData,
|
||||
@@ -208,16 +208,21 @@ export default class NotificationBackground {
|
||||
organizations.find((org) => org.id === orgId)?.productTierType;
|
||||
|
||||
const cipherQueueMessage = this.notificationQueue.find(
|
||||
(message): message is AddChangePasswordQueueMessage | AddLoginQueueMessage =>
|
||||
message.type === NotificationQueueMessageType.ChangePassword ||
|
||||
message.type === NotificationQueueMessageType.AddLogin,
|
||||
(message): message is AddChangePasswordNotificationQueueMessage | AddLoginQueueMessage =>
|
||||
message.type === NotificationType.ChangePassword ||
|
||||
message.type === NotificationType.AddLogin,
|
||||
);
|
||||
|
||||
if (cipherQueueMessage) {
|
||||
const cipherView =
|
||||
cipherQueueMessage.type === NotificationQueueMessageType.ChangePassword
|
||||
? await this.getDecryptedCipherById(cipherQueueMessage.cipherId, activeUserId)
|
||||
: this.convertAddLoginQueueMessageToCipherView(cipherQueueMessage);
|
||||
let cipherView: CipherView;
|
||||
if (cipherQueueMessage.type === NotificationType.ChangePassword) {
|
||||
const {
|
||||
data: { cipherId },
|
||||
} = cipherQueueMessage;
|
||||
cipherView = await this.getDecryptedCipherById(cipherId, activeUserId);
|
||||
} else {
|
||||
cipherView = this.convertAddLoginQueueMessageToCipherView(cipherQueueMessage);
|
||||
}
|
||||
|
||||
const organizationType = getOrganizationType(cipherView.organizationId);
|
||||
return [
|
||||
@@ -424,7 +429,7 @@ export default class NotificationBackground {
|
||||
};
|
||||
|
||||
switch (notificationType) {
|
||||
case NotificationQueueMessageType.AddLogin:
|
||||
case NotificationType.AddLogin:
|
||||
typeData.removeIndividualVault = await this.removeIndividualVault();
|
||||
break;
|
||||
}
|
||||
@@ -501,7 +506,7 @@ export default class NotificationBackground {
|
||||
const queueMessage: NotificationQueueMessageItem = {
|
||||
domain,
|
||||
wasVaultLocked,
|
||||
type: NotificationQueueMessageType.AtRiskPassword,
|
||||
type: NotificationType.AtRiskPassword,
|
||||
passwordChangeUri,
|
||||
organizationName: organization.name,
|
||||
tab: tab,
|
||||
@@ -591,7 +596,7 @@ export default class NotificationBackground {
|
||||
this.removeTabFromNotificationQueue(tab);
|
||||
const launchTimestamp = new Date().getTime();
|
||||
const message: AddLoginQueueMessage = {
|
||||
type: NotificationQueueMessageType.AddLogin,
|
||||
type: NotificationType.AddLogin,
|
||||
username: loginInfo.username,
|
||||
password: loginInfo.password,
|
||||
domain: loginDomain,
|
||||
@@ -716,10 +721,9 @@ export default class NotificationBackground {
|
||||
// remove any old messages for this tab
|
||||
this.removeTabFromNotificationQueue(tab);
|
||||
const launchTimestamp = new Date().getTime();
|
||||
const message: AddChangePasswordQueueMessage = {
|
||||
type: NotificationQueueMessageType.ChangePassword,
|
||||
cipherId: cipherId,
|
||||
newPassword: newPassword,
|
||||
const message: AddChangePasswordNotificationQueueMessage = {
|
||||
type: NotificationType.ChangePassword,
|
||||
data: { cipherId: cipherId, newPassword: newPassword },
|
||||
domain: loginDomain,
|
||||
tab: tab,
|
||||
launchTimestamp,
|
||||
@@ -734,7 +738,7 @@ export default class NotificationBackground {
|
||||
this.removeTabFromNotificationQueue(tab);
|
||||
const launchTimestamp = new Date().getTime();
|
||||
const message: AddUnlockVaultQueueMessage = {
|
||||
type: NotificationQueueMessageType.UnlockVault,
|
||||
type: NotificationType.UnlockVault,
|
||||
domain: loginDomain,
|
||||
tab: tab,
|
||||
launchTimestamp,
|
||||
@@ -804,8 +808,8 @@ export default class NotificationBackground {
|
||||
const queueMessage = this.notificationQueue[i];
|
||||
if (
|
||||
queueMessage.tab.id !== tab.id ||
|
||||
(queueMessage.type !== NotificationQueueMessageType.AddLogin &&
|
||||
queueMessage.type !== NotificationQueueMessageType.ChangePassword)
|
||||
(queueMessage.type !== NotificationType.AddLogin &&
|
||||
queueMessage.type !== NotificationType.ChangePassword)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -818,17 +822,13 @@ export default class NotificationBackground {
|
||||
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
||||
);
|
||||
|
||||
if (queueMessage.type === NotificationQueueMessageType.ChangePassword) {
|
||||
const cipherView = await this.getDecryptedCipherById(queueMessage.cipherId, activeUserId);
|
||||
if (queueMessage.type === NotificationType.ChangePassword) {
|
||||
const {
|
||||
data: { cipherId, newPassword },
|
||||
} = queueMessage;
|
||||
const cipherView = await this.getDecryptedCipherById(cipherId, activeUserId);
|
||||
|
||||
await this.updatePassword(
|
||||
cipherView,
|
||||
queueMessage.newPassword,
|
||||
edit,
|
||||
tab,
|
||||
activeUserId,
|
||||
skipReprompt,
|
||||
);
|
||||
await this.updatePassword(cipherView, newPassword, edit, tab, activeUserId, skipReprompt);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -993,7 +993,7 @@ export default class NotificationBackground {
|
||||
|
||||
const queueItem = this.notificationQueue.find((item) => item.tab.id === senderTab.id);
|
||||
|
||||
if (queueItem?.type === NotificationQueueMessageType.AddLogin) {
|
||||
if (queueItem?.type === NotificationType.AddLogin) {
|
||||
const cipherView = this.convertAddLoginQueueMessageToCipherView(queueItem);
|
||||
cipherView.organizationId = organizationId;
|
||||
cipherView.folderId = folder;
|
||||
@@ -1075,10 +1075,7 @@ export default class NotificationBackground {
|
||||
private async saveNever(tab: chrome.tabs.Tab) {
|
||||
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
|
||||
const queueMessage = this.notificationQueue[i];
|
||||
if (
|
||||
queueMessage.tab.id !== tab.id ||
|
||||
queueMessage.type !== NotificationQueueMessageType.AddLogin
|
||||
) {
|
||||
if (queueMessage.tab.id !== tab.id || queueMessage.type !== NotificationType.AddLogin) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1866,7 +1866,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
|
||||
frameId: this.focusedFieldData.frameId || 0,
|
||||
},
|
||||
);
|
||||
}, 150);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
const NotificationQueueMessageType = {
|
||||
AddLogin: "add",
|
||||
ChangePassword: "change",
|
||||
UnlockVault: "unlock",
|
||||
AtRiskPassword: "at-risk-password",
|
||||
} as const;
|
||||
|
||||
type NotificationQueueMessageTypes =
|
||||
(typeof NotificationQueueMessageType)[keyof typeof NotificationQueueMessageType];
|
||||
|
||||
export { NotificationQueueMessageType, NotificationQueueMessageTypes };
|
||||
10
apps/browser/src/autofill/enums/notification-type.enum.ts
Normal file
10
apps/browser/src/autofill/enums/notification-type.enum.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
const NotificationType = {
|
||||
AddLogin: "add",
|
||||
ChangePassword: "change",
|
||||
UnlockVault: "unlock",
|
||||
AtRiskPassword: "at-risk-password",
|
||||
} as const;
|
||||
|
||||
type NotificationTypes = (typeof NotificationType)[keyof typeof NotificationType];
|
||||
|
||||
export { NotificationType, NotificationTypes };
|
||||
@@ -14,6 +14,10 @@ const NotificationTypes = {
|
||||
AtRiskPassword: "at-risk-password",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* @todo Deprecate in favor of apps/browser/src/autofill/enums/notification-type.enum.ts
|
||||
* - Determine fix or workaround for restricted imports of that file.
|
||||
*/
|
||||
type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes];
|
||||
|
||||
type NotificationTaskInfo = {
|
||||
@@ -21,6 +25,9 @@ type NotificationTaskInfo = {
|
||||
remainingTasksCount: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* @todo Use generics to make this type specific to notification types, see Standard_NotificationQueueMessage.
|
||||
*/
|
||||
type NotificationBarIframeInitData = {
|
||||
ciphers?: NotificationCipherData[];
|
||||
folders?: FolderView[];
|
||||
|
||||
Reference in New Issue
Block a user