mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-21 07:46:06 +08:00
Remove work window, connect to CPP
This commit is contained in:
parent
a5afcf17ee
commit
48fbe1fbac
|
@ -190,19 +190,18 @@ export default class Application extends EventEmitter {
|
|||
}
|
||||
|
||||
openWindowsForTokenState() {
|
||||
const accounts = this.config.get('nylas.accounts');
|
||||
const hasAccount = accounts && accounts.length > 0;
|
||||
const hasN1ID = this._getNylasId();
|
||||
// const accounts = this.config.get('nylas.accounts');
|
||||
// const hasAccount = accounts && accounts.length > 0;
|
||||
// const hasN1ID = this._getNylasId();
|
||||
|
||||
if (hasAccount && hasN1ID) {
|
||||
this.windowManager.ensureWindow(WindowManager.MAIN_WINDOW);
|
||||
this.windowManager.ensureWindow(WindowManager.WORK_WINDOW);
|
||||
} else {
|
||||
this.windowManager.ensureWindow(WindowManager.ONBOARDING_WINDOW, {
|
||||
title: "Welcome to Nylas Mail",
|
||||
});
|
||||
this.windowManager.ensureWindow(WindowManager.WORK_WINDOW);
|
||||
}
|
||||
// TODO BEN
|
||||
// if (hasAccount && hasN1ID) {
|
||||
this.windowManager.ensureWindow(WindowManager.MAIN_WINDOW);
|
||||
// } else {
|
||||
// this.windowManager.ensureWindow(WindowManager.ONBOARDING_WINDOW, {
|
||||
// title: "Welcome to Nylas Mail",
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
_getNylasId() {
|
||||
|
@ -503,7 +502,8 @@ export default class Application extends EventEmitter {
|
|||
});
|
||||
|
||||
ipcMain.on('ensure-worker-window', () => {
|
||||
this.windowManager.ensureWindow(WindowManager.WORK_WINDOW)
|
||||
// TODO BG
|
||||
// this.windowManager.ensureWindow(WindowManager.WORK_WINDOW)
|
||||
})
|
||||
|
||||
ipcMain.on('inline-style-parse', (event, {html, key}) => {
|
||||
|
@ -571,14 +571,15 @@ export default class Application extends EventEmitter {
|
|||
});
|
||||
|
||||
ipcMain.on('action-bridge-rebroadcast-to-work', (event, ...args) => {
|
||||
const workWindow = this.windowManager.get(WindowManager.WORK_WINDOW)
|
||||
if (!workWindow || !workWindow.browserWindow.webContents) {
|
||||
return;
|
||||
}
|
||||
if (BrowserWindow.fromWebContents(event.sender) === workWindow) {
|
||||
return;
|
||||
}
|
||||
workWindow.browserWindow.webContents.send('action-bridge-message', ...args);
|
||||
// TODO BG
|
||||
// const workWindow = this.windowManager.get(WindowManager.WORK_WINDOW)
|
||||
// if (!workWindow || !workWindow.browserWindow.webContents) {
|
||||
// return;
|
||||
// }
|
||||
// if (BrowserWindow.fromWebContents(event.sender) === workWindow) {
|
||||
// return;
|
||||
// }
|
||||
// workWindow.browserWindow.webContents.send('action-bridge-message', ...args);
|
||||
});
|
||||
|
||||
ipcMain.on('write-text-to-selection-clipboard', (event, selectedText) => {
|
||||
|
@ -596,7 +597,8 @@ export default class Application extends EventEmitter {
|
|||
});
|
||||
|
||||
ipcMain.on('new-account-added', () => {
|
||||
this.windowManager.ensureWindow(WindowManager.WORK_WINDOW)
|
||||
// TODO BEN
|
||||
// this.windowManager.ensureWindow(WindowManager.WORK_WINDOW)
|
||||
});
|
||||
|
||||
ipcMain.on('run-in-window', (event, params) => {
|
||||
|
@ -605,7 +607,6 @@ export default class Application extends EventEmitter {
|
|||
this._sourceWindows[params.taskId] = sourceWindow
|
||||
|
||||
const targetWindowKey = {
|
||||
work: WindowManager.WORK_WINDOW,
|
||||
main: WindowManager.MAIN_WINDOW,
|
||||
}[params.window];
|
||||
if (!targetWindowKey) {
|
||||
|
|
|
@ -3,7 +3,6 @@ import {app} from 'electron';
|
|||
import WindowLauncher from './window-launcher';
|
||||
|
||||
const MAIN_WINDOW = "default"
|
||||
const WORK_WINDOW = "work"
|
||||
const SPEC_WINDOW = "spec"
|
||||
const ONBOARDING_WINDOW = "onboarding"
|
||||
// const CALENDAR_WINDOW = "calendar"
|
||||
|
@ -195,17 +194,6 @@ export default class WindowManager {
|
|||
initializeInBackground: this.initializeInBackground,
|
||||
};
|
||||
|
||||
coreWinOpts[WindowManager.WORK_WINDOW] = {
|
||||
windowKey: WindowManager.WORK_WINDOW,
|
||||
windowType: WindowManager.WORK_WINDOW,
|
||||
coldStartOnly: true, // It's a secondary window, but not a hot window
|
||||
title: "Activity",
|
||||
hidden: true,
|
||||
neverClose: true,
|
||||
width: 800,
|
||||
height: 400,
|
||||
}
|
||||
|
||||
coreWinOpts[WindowManager.ONBOARDING_WINDOW] = {
|
||||
windowKey: WindowManager.ONBOARDING_WINDOW,
|
||||
windowType: WindowManager.ONBOARDING_WINDOW,
|
||||
|
@ -238,7 +226,6 @@ export default class WindowManager {
|
|||
}
|
||||
|
||||
WindowManager.MAIN_WINDOW = MAIN_WINDOW;
|
||||
WindowManager.WORK_WINDOW = WORK_WINDOW;
|
||||
WindowManager.SPEC_WINDOW = SPEC_WINDOW;
|
||||
// WindowManager.CALENDAR_WINDOW = CALENDAR_WINDOW;
|
||||
WindowManager.ONBOARDING_WINDOW = ONBOARDING_WINDOW;
|
||||
|
|
84
packages/client-app/src/flux/action-bridge-cpp.es6
Normal file
84
packages/client-app/src/flux/action-bridge-cpp.es6
Normal file
|
@ -0,0 +1,84 @@
|
|||
import _ from 'underscore';
|
||||
import net from 'net';
|
||||
import fs from 'fs';
|
||||
import Actions from './actions';
|
||||
import DatabaseStore from './stores/database-store';
|
||||
import DatabaseChangeRecord from './stores/database-change-record';
|
||||
|
||||
import Utils from './models/utils';
|
||||
|
||||
const Message = {
|
||||
DATABASE_STORE_TRIGGER: 'db-store-trigger',
|
||||
};
|
||||
|
||||
const printToConsole = false;
|
||||
|
||||
class ActionBridgeCPP {
|
||||
|
||||
constructor() {
|
||||
if (!NylasEnv.isMainWindow()) {
|
||||
// maybe bind as listener?
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
fs.unlinkSync('/tmp/cmail.sock');
|
||||
} catch (err) {
|
||||
console.info(err);
|
||||
}
|
||||
|
||||
// This server listens on a Unix socket at /var/run/mysocket
|
||||
const unixServer = net.createServer((c) => {
|
||||
// Do something with the client connection
|
||||
console.log('client connected');
|
||||
c.on('data', (d) => {
|
||||
this.onIncomingMessage(d.toString());
|
||||
});
|
||||
c.on('end', () => {
|
||||
console.log('client disconnected');
|
||||
});
|
||||
c.write('hello\r\n');
|
||||
c.pipe(c);
|
||||
});
|
||||
|
||||
unixServer.listen('/tmp/cmail.sock', () => {
|
||||
console.log('server bound');
|
||||
});
|
||||
|
||||
function shutdown() {
|
||||
unixServer.close(); // socket file is automatically removed here
|
||||
process.exit();
|
||||
}
|
||||
|
||||
this._readBuffer = '';
|
||||
process.on('SIGINT', shutdown);
|
||||
}
|
||||
|
||||
onIncomingMessage(message) {
|
||||
console.log(message);
|
||||
this._readBuffer += message;
|
||||
const msgs = this._readBuffer.split('\n');
|
||||
this._readBuffer = msgs.pop();
|
||||
|
||||
for (const msg of msgs) {
|
||||
const {type, model} = JSON.parse(msg, Utils.registeredObjectReviver);
|
||||
DatabaseStore.triggeringFromActionBridge = true;
|
||||
DatabaseStore.trigger(new DatabaseChangeRecord({type, objects: [model]}));
|
||||
DatabaseStore.triggeringFromActionBridge = false;
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnload(readyToUnload) {
|
||||
// Unfortunately, if you call ipc.send and then immediately close the window,
|
||||
// Electron won't actually send the message. To work around this, we wait an
|
||||
// arbitrary amount of time before closing the window after the last IPC event
|
||||
// was sent. https://github.com/atom/electron/issues/4366
|
||||
if (this.ipcLastSendTime && Date.now() - this.ipcLastSendTime < 100) {
|
||||
setTimeout(readyToUnload, 100);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default ActionBridgeCPP;
|
|
@ -106,8 +106,8 @@ export default class Category extends Model {
|
|||
static additionalSQLiteConfig = {
|
||||
setup: () => {
|
||||
return [
|
||||
'CREATE INDEX IF NOT EXISTS CategoryNameIndex ON Category(account_id,name)',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS CategoryClientIndex ON Category(client_id)',
|
||||
'CREATE INDEX IF NOT EXISTS CategoryNameIndex ON Category(accountId,name)',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS CategoryClientIndex ON Category(id)',
|
||||
];
|
||||
},
|
||||
};
|
||||
|
|
|
@ -79,7 +79,6 @@ export default class Contact extends Model {
|
|||
isSearchIndexed: Attributes.Boolean({
|
||||
queryable: true,
|
||||
modelKey: 'isSearchIndexed',
|
||||
jsonKey: 'is_search_indexed',
|
||||
defaultValue: false,
|
||||
}),
|
||||
|
||||
|
@ -88,7 +87,6 @@ export default class Contact extends Model {
|
|||
// these operations would be way too slow on large FTS tables.
|
||||
searchIndexId: Attributes.Number({
|
||||
modelKey: 'searchIndexId',
|
||||
jsonKey: 'search_index_id',
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -96,8 +94,8 @@ export default class Contact extends Model {
|
|||
setup: () => {
|
||||
return [
|
||||
'CREATE INDEX IF NOT EXISTS ContactEmailIndex ON Contact(email)',
|
||||
'CREATE INDEX IF NOT EXISTS ContactAccountEmailIndex ON Contact(account_id, email)',
|
||||
'CREATE INDEX IF NOT EXISTS ContactIsSearchIndexedIndex ON `Contact` (is_search_indexed, id)',
|
||||
'CREATE INDEX IF NOT EXISTS ContactAccountEmailIndex ON Contact(accountId, email)',
|
||||
'CREATE INDEX IF NOT EXISTS ContactIsSearchIndexedIndex ON `Contact` (isSearchIndexed, id)',
|
||||
];
|
||||
},
|
||||
};
|
||||
|
|
|
@ -130,7 +130,7 @@ export default class Event extends Model {
|
|||
static additionalSQLiteConfig = {
|
||||
setup: () => {
|
||||
return [
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS EventClientIndex ON Event(client_id)',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS EventClientIndex ON Event(id)',
|
||||
'CREATE INDEX IF NOT EXISTS EventIsSearchIndexedIndex ON `Event` (is_search_indexed, id)',
|
||||
];
|
||||
},
|
||||
|
|
|
@ -93,7 +93,6 @@ export default class Message extends ModelWithMetadata {
|
|||
|
||||
replyTo: Attributes.Collection({
|
||||
modelKey: 'replyTo',
|
||||
jsonKey: 'reply_to',
|
||||
itemClass: Contact,
|
||||
}),
|
||||
|
||||
|
@ -153,12 +152,10 @@ export default class Message extends ModelWithMetadata {
|
|||
threadId: Attributes.ServerId({
|
||||
queryable: true,
|
||||
modelKey: 'threadId',
|
||||
jsonKey: 'thread_id',
|
||||
}),
|
||||
|
||||
messageIdHeader: Attributes.ServerId({
|
||||
modelKey: 'messageIdHeader',
|
||||
jsonKey: 'message_id_header',
|
||||
}),
|
||||
|
||||
subject: Attributes.String({
|
||||
|
@ -167,13 +164,11 @@ export default class Message extends ModelWithMetadata {
|
|||
|
||||
draft: Attributes.Boolean({
|
||||
modelKey: 'draft',
|
||||
jsonKey: 'draft',
|
||||
queryable: true,
|
||||
}),
|
||||
|
||||
pristine: Attributes.Boolean({
|
||||
modelKey: 'pristine',
|
||||
jsonKey: 'pristine',
|
||||
queryable: false,
|
||||
}),
|
||||
|
||||
|
@ -184,7 +179,6 @@ export default class Message extends ModelWithMetadata {
|
|||
|
||||
replyToMessageId: Attributes.ServerId({
|
||||
modelKey: 'replyToMessageId',
|
||||
jsonKey: 'reply_to_message_id',
|
||||
}),
|
||||
|
||||
categories: Attributes.Collection({
|
||||
|
@ -199,10 +193,10 @@ export default class Message extends ModelWithMetadata {
|
|||
|
||||
static additionalSQLiteConfig = {
|
||||
setup: () => [
|
||||
`CREATE INDEX IF NOT EXISTS MessageListThreadIndex ON Message(thread_id, date ASC)`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS MessageDraftIndex ON Message(client_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS MessageListThreadIndex ON Message(threadId, date ASC)`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS MessageDraftIndex ON Message(id)`,
|
||||
`CREATE INDEX IF NOT EXISTS MessageListDraftIndex ON \
|
||||
Message(account_id, date DESC) WHERE draft = 1`,
|
||||
Message(accountId, date DESC) WHERE draft = 1`,
|
||||
`CREATE INDEX IF NOT EXISTS MessageListUnifiedDraftIndex ON \
|
||||
Message(date DESC) WHERE draft = 1`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS MessageBodyIndex ON MessageBody(id)`,
|
||||
|
|
|
@ -31,11 +31,13 @@ Section: Models
|
|||
###
|
||||
class Model
|
||||
|
||||
Object.defineProperty @prototype, "id",
|
||||
Object.defineProperty @prototype, "clientId",
|
||||
enumerable: false
|
||||
get: -> @serverId ? @clientId
|
||||
set: ->
|
||||
throw new Error("You may not directly set the ID of an object. Set either the `clientId` or the `serverId` instead.")
|
||||
get: -> @id
|
||||
|
||||
Object.defineProperty @prototype, "serverId",
|
||||
enumerable: false
|
||||
get: -> @id
|
||||
|
||||
@attributes:
|
||||
# Lookups will go through the custom getter.
|
||||
|
@ -43,22 +45,12 @@ class Model
|
|||
queryable: true
|
||||
modelKey: 'id'
|
||||
|
||||
'clientId': Attributes.String
|
||||
queryable: true
|
||||
modelKey: 'clientId'
|
||||
jsonKey: 'client_id'
|
||||
|
||||
'serverId': Attributes.ServerId
|
||||
modelKey: 'serverId'
|
||||
jsonKey: 'server_id'
|
||||
|
||||
'object': Attributes.String
|
||||
modelKey: 'object'
|
||||
|
||||
'accountId': Attributes.ServerId
|
||||
queryable: true
|
||||
modelKey: 'accountId'
|
||||
jsonKey: 'account_id'
|
||||
|
||||
@naturalSortOrder: -> null
|
||||
|
||||
|
|
|
@ -85,31 +85,27 @@ class Thread extends ModelWithMetadata {
|
|||
}),
|
||||
|
||||
hasAttachments: Attributes.Boolean({
|
||||
modelKey: 'has_attachments',
|
||||
modelKey: 'hasAttachments',
|
||||
}),
|
||||
|
||||
lastMessageReceivedTimestamp: Attributes.DateTime({
|
||||
queryable: true,
|
||||
modelKey: 'lastMessageReceivedTimestamp',
|
||||
jsonKey: 'last_message_received_timestamp',
|
||||
}),
|
||||
|
||||
lastMessageSentTimestamp: Attributes.DateTime({
|
||||
queryable: true,
|
||||
modelKey: 'lastMessageSentTimestamp',
|
||||
jsonKey: 'last_message_sent_timestamp',
|
||||
}),
|
||||
|
||||
inAllMail: Attributes.Boolean({
|
||||
queryable: true,
|
||||
modelKey: 'inAllMail',
|
||||
jsonKey: 'in_all_mail',
|
||||
}),
|
||||
|
||||
isSearchIndexed: Attributes.Boolean({
|
||||
queryable: true,
|
||||
modelKey: 'isSearchIndexed',
|
||||
jsonKey: 'is_search_indexed',
|
||||
defaultValue: false,
|
||||
loadFromColumn: true,
|
||||
}),
|
||||
|
@ -119,7 +115,6 @@ class Thread extends ModelWithMetadata {
|
|||
// these operations would be way too slow on large FTS tables.
|
||||
searchIndexId: Attributes.Number({
|
||||
modelKey: 'searchIndexId',
|
||||
jsonKey: 'search_index_id',
|
||||
}),
|
||||
})
|
||||
|
||||
|
@ -138,26 +133,26 @@ class Thread extends ModelWithMetadata {
|
|||
'CREATE UNIQUE INDEX IF NOT EXISTS ThreadCountsIndex ON `ThreadCounts` (category_id DESC)',
|
||||
|
||||
// ThreadContact
|
||||
'CREATE INDEX IF NOT EXISTS ThreadContactDateIndex ON `ThreadContact` (last_message_received_timestamp DESC, value, id)',
|
||||
'CREATE INDEX IF NOT EXISTS ThreadContactDateIndex ON `ThreadContact` (lastMessageReceivedTimestamp DESC, value, id)',
|
||||
|
||||
// ThreadCategory
|
||||
'CREATE INDEX IF NOT EXISTS ThreadListCategoryIndex ON `ThreadCategory` (last_message_received_timestamp DESC, value, in_all_mail, unread, id)',
|
||||
'CREATE INDEX IF NOT EXISTS ThreadListCategorySentIndex ON `ThreadCategory` (last_message_sent_timestamp DESC, value, in_all_mail, unread, id)',
|
||||
'CREATE INDEX IF NOT EXISTS ThreadListCategoryIndex ON `ThreadCategory` (lastMessageReceivedTimestamp DESC, value, inAllMail, unread, id)',
|
||||
'CREATE INDEX IF NOT EXISTS ThreadListCategorySentIndex ON `ThreadCategory` (lastMessageSentTimestamp DESC, value, inAllMail, unread, id)',
|
||||
|
||||
// Thread: General index
|
||||
'CREATE INDEX IF NOT EXISTS ThreadDateIndex ON `Thread` (last_message_received_timestamp DESC)',
|
||||
'CREATE INDEX IF NOT EXISTS ThreadClientIdIndex ON `Thread` (client_id)',
|
||||
'CREATE INDEX IF NOT EXISTS ThreadDateIndex ON `Thread` (lastMessageReceivedTimestamp DESC)',
|
||||
'CREATE INDEX IF NOT EXISTS ThreadClientIdIndex ON `Thread` (id)',
|
||||
|
||||
// Thread: Partial indexes for specific views
|
||||
'CREATE INDEX IF NOT EXISTS ThreadUnreadIndex ON `Thread` (account_id, last_message_received_timestamp DESC) WHERE unread = 1 AND in_all_mail = 1',
|
||||
'CREATE INDEX IF NOT EXISTS ThreadUnifiedUnreadIndex ON `Thread` (last_message_received_timestamp DESC) WHERE unread = 1 AND in_all_mail = 1',
|
||||
'CREATE INDEX IF NOT EXISTS ThreadUnreadIndex ON `Thread` (accountId, lastMessageReceivedTimestamp DESC) WHERE unread = 1 AND inAllMail = 1',
|
||||
'CREATE INDEX IF NOT EXISTS ThreadUnifiedUnreadIndex ON `Thread` (lastMessageReceivedTimestamp DESC) WHERE unread = 1 AND inAllMail = 1',
|
||||
|
||||
'DROP INDEX IF EXISTS `Thread`.ThreadStarIndex',
|
||||
'CREATE INDEX IF NOT EXISTS ThreadStarredIndex ON `Thread` (account_id, last_message_received_timestamp DESC) WHERE starred = 1 AND in_all_mail = 1',
|
||||
'CREATE INDEX IF NOT EXISTS ThreadUnifiedStarredIndex ON `Thread` (last_message_received_timestamp DESC) WHERE starred = 1 AND in_all_mail = 1',
|
||||
'CREATE INDEX IF NOT EXISTS ThreadStarredIndex ON `Thread` (accountId, lastMessageReceivedTimestamp DESC) WHERE starred = 1 AND inAllMail = 1',
|
||||
'CREATE INDEX IF NOT EXISTS ThreadUnifiedStarredIndex ON `Thread` (lastMessageReceivedTimestamp DESC) WHERE starred = 1 AND inAllMail = 1',
|
||||
|
||||
'CREATE INDEX IF NOT EXISTS ThreadIsSearchIndexedIndex ON `Thread` (is_search_indexed, id)',
|
||||
'CREATE INDEX IF NOT EXISTS ThreadIsSearchIndexedLastMessageReceivedIndex ON `Thread` (is_search_indexed, last_message_received_timestamp)',
|
||||
'CREATE INDEX IF NOT EXISTS ThreadIsSearchIndexedIndex ON `Thread` (isSearchIndexed, id)',
|
||||
'CREATE INDEX IF NOT EXISTS ThreadIsSearchIndexedLastMessageReceivedIndex ON `Thread` (isSearchIndexed, lastMessageReceivedTimestamp)',
|
||||
],
|
||||
}
|
||||
|
||||
|
|
|
@ -129,7 +129,7 @@ class DatabaseStore extends NylasStore {
|
|||
const app = remote.getGlobal('application')
|
||||
const phase = app.databasePhase()
|
||||
|
||||
if (phase === DatabasePhase.Setup && NylasEnv.isWorkWindow()) {
|
||||
if (phase === DatabasePhase.Setup && NylasEnv.isMainWindow()) {
|
||||
await this._openDatabase()
|
||||
this._checkDatabaseVersion({allowUnset: true}, () => {
|
||||
this._runDatabaseSetup(() => {
|
||||
|
@ -163,7 +163,7 @@ class DatabaseStore extends NylasStore {
|
|||
// database schema to prepare those tables. This method may be called
|
||||
// extremely frequently as new models are added when packages load.
|
||||
refreshDatabaseSchema() {
|
||||
if (!NylasEnv.isWorkWindow()) {
|
||||
if (!NylasEnv.isMainWindow()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const app = remote.getGlobal('application');
|
||||
|
|
|
@ -172,6 +172,7 @@ export default class NylasEnvConstructor {
|
|||
const ThemeManager = require('./theme-manager');
|
||||
const StyleManager = require('./style-manager');
|
||||
const ActionBridge = require('./flux/action-bridge').default;
|
||||
const ActionBridgeCPP = require('./flux/action-bridge-cpp').default;
|
||||
const MenuManager = require('./menu-manager').default;
|
||||
|
||||
const {devMode, benchmarkMode, safeMode, resourcePath, configDirPath, windowType} = this.getLoadSettings();
|
||||
|
@ -229,6 +230,7 @@ export default class NylasEnvConstructor {
|
|||
this.globalWindowEmitter = new Emitter();
|
||||
|
||||
if (!this.inSpecMode()) {
|
||||
this.actionBridgeCpp = new ActionBridgeCPP();
|
||||
this.actionBridge = new ActionBridge(ipcRenderer);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue