Plugin
MLEnhanced
G'day
1
/*2
* FiveCord — a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
export const VERSION = "3.0.0";8
9
export const Native = getNative();10
11
import "./styles.css";12
13
import { NavContextMenuPatchCallback } from "@api/ContextMenu";14
import { definePluginSettings, Settings } from "@api/Settings";15
import ErrorBoundary from "@components/ErrorBoundary";16
import { Devs } from "@utils/constants";17
import { Logger } from "@utils/Logger";18
import definePlugin, { OptionType } from "@utils/types";19
import { findByPropsLazy } from "@webpack";20
import { Alerts, Button, FluxDispatcher, Menu, MessageActions, MessageStore, React, Toasts, UserStore } from "@webpack/common";21
22
import { OpenLogsButton } from "./components/LogsButton";23
import { openLogModal } from "./components/LogsModal";24
import { ImageCacheDir, LogsDir } from "./components/settings/FolderSelectInput";25
import * as LoggedMessageManager from "./LoggedMessageManager";26
import { addMessage, loggedMessages, MessageLoggerStore, removeLog } from "./LoggedMessageManager";27
import { LoadMessagePayload, LoggedAttachment, LoggedMessage, LoggedMessageJSON, MessageCreatePayload, MessageDeleteBulkPayload, MessageDeletePayload, MessageUpdatePayload } from "./types";28
import { addToXAndRemoveFromOpposite, cleanUpCachedMessage, cleanupUserObject, doesBlobUrlExist, getNative, isGhostPinged, ListType, mapEditHistory, reAddDeletedMessages, removeFromX } from "./utils";29
import { DEFAULT_IMAGE_CACHE_DIR } from "./utils/constants";30
import { shouldIgnore } from "./utils/index";31
import { LimitedMap } from "./utils/LimitedMap";32
import { doesMatch } from "./utils/parseQuery";33
import * as imageUtils from "./utils/saveImage";34
import * as ImageManager from "./utils/saveImage/ImageManager";35
import { downloadLoggedMessages } from "./utils/settingsUtils";36
37
38
export const Flogger = new Logger("MessageLoggerEnhanced", "#f26c6c");39
40
export const cacheSentMessages = new LimitedMap<string, LoggedMessageJSON>();41
42
const cacheThing = findByPropsLazy("commit", "getOrCreate");43
44
45
const handledMessageIds = new Set();46
async function messageDeleteHandler(payload: MessageDeletePayload & { isBulk: boolean; }) {47
if (payload.mlDeleted) {48
if (settings.store.permanentlyRemoveLogByDefault)49
await removeLog(payload.id);50
51
return;52
}53
54
if (handledMessageIds.has(payload.id)) {55
// Flogger.warn("skipping duplicate message", payload.id);56
return;57
}58
59
try {60
handledMessageIds.add(payload.id);61
62
let message: LoggedMessage | LoggedMessageJSON | null =63
MessageStore.getMessage(payload.channelId, payload.id);64
if (message == null) {65
// most likely an edited message66
const cachedMessage = cacheSentMessages.get(`${payload.channelId},${payload.id}`);67
if (!cachedMessage) return; class="ts-cmt">// Flogger.log("no message to save");68
69
message = { ...cacheSentMessages.get(`${payload.channelId},${payload.id}`), deleted: true } as LoggedMessageJSON;70
}71
72
if (73
shouldIgnore({74
channelId: message?.channel_id ?? payload.channelId,75
guildId: payload.guildId ?? (message as any).guildId ?? (message as any).guild_id,76
authorId: message?.author?.id,77
bot: message?.bot || message?.author?.bot,78
flags: message?.flags,79
ghostPinged: isGhostPinged(message as any),80
isCachedByUs: (message as LoggedMessageJSON).ourCache81
})82
) {83
// Flogger.log("IGNORING", message, payload);84
return FluxDispatcher.dispatch({85
type: "MESSAGE_DELETE",86
channelId: payload.channelId,87
id: payload.id,88
mlDeleted: true89
});90
}91
92
93
if (message == null || message.channel_id == null || !message.deleted) return;94
// Flogger.log("ADDING MESSAGE (DELETED)", message);95
await addMessage(message, "deletedMessages", payload.isBulk ?? false);96
}97
finally {98
handledMessageIds.delete(payload.id);99
}100
}101
102
async function messageDeleteBulkHandler({ channelId, guildId, ids }: MessageDeleteBulkPayload) {103
// is this bad? idk man104
for (const id of ids) {105
await messageDeleteHandler({ type: "MESSAGE_DELETE", channelId, guildId, id, isBulk: true });106
}107
await LoggedMessageManager.saveLoggedMessages();108
}109
110
async function messageUpdateHandler(payload: MessageUpdatePayload) {111
const cachedMessage = cacheSentMessages.get(`${payload.message.channel_id},${payload.message.id}`);112
if (113
shouldIgnore({114
channelId: payload.message?.channel_id,115
guildId: payload.guildId ?? (payload as any).guild_id,116
authorId: payload.message?.author?.id,117
bot: (payload.message?.author as any)?.bot,118
flags: payload.message?.flags,119
ghostPinged: isGhostPinged(payload.message as any),120
isCachedByUs: cachedMessage?.ourCache ?? false121
})122
) {123
const cache = cacheThing.getOrCreate(payload.message.channel_id);124
const message = cache.get(payload.message.id);125
if (message) {126
message.editHistory = [];127
cacheThing.commit(cache);128
}129
return;class="ts-cmt">// Flogger.log("this message has been ignored", payload);130
}131
132
let message = MessageStore133
.getMessage(payload.message.channel_id, payload.message.id) as LoggedMessage | LoggedMessageJSON | null;134
135
if (message == null) {136
// MESSAGE_UPDATE gets dispatched when emebeds change too and content becomes null137
if (cachedMessage != null && payload.message.content != null && cachedMessage.content !== payload.message.content) {138
message = {139
...cachedMessage,140
content: payload.message.content,141
editHistory: [142
...(cachedMessage.editHistory ?? []),143
{144
content: cachedMessage.content,145
timestamp: (new Date()).toISOString()146
}147
]148
};149
150
cacheSentMessages.set(`${payload.message.channel_id},${payload.message.id}`, message);151
}152
}153
154
if (message == null || message.channel_id == null || message.editHistory == null || message.editHistory.length === 0) return;155
156
// Flogger.log("ADDING MESSAGE (EDITED)", message, payload);157
await addMessage(message, "editedMessages");158
}159
160
function messageCreateHandler(payload: MessageCreatePayload) {161
// we do this here because cache is limited and to save memory162
if (!settings.store.cacheMessagesFromServers && payload.guildId != null) {163
const ids = [payload.channelId, payload.message?.author?.id, payload.guildId];164
const isWhitelisted =165
settings.store.whitelistedIds166
.split(",")167
.some(e => ids.includes(e));168
if (!isWhitelisted) {169
return; class="ts-cmt">// dont cache messages from servers when cacheMessagesFromServers is disabled and not whitelisted.170
}171
}172
173
cacheSentMessages.set(`${payload.message.channel_id},${payload.message.id}`, cleanUpCachedMessage(payload.message));174
// Flogger.log(`cached\nkey:${payload.message.channel_id},${payload.message.id}\nvalue:`, payload.message);175
}176
177
// also stolen from mlv2178
function messageLoadSuccess(payload: LoadMessagePayload) {179
const deletedMessages = loggedMessages.deletedMessages[payload.channelId];180
const editedMessages = loggedMessages.editedMessages[payload.channelId];181
const recordIDs: string[] = [...(deletedMessages || []), ...(editedMessages || [])];182
183
184
for (let i = 0; i < payload.messages.length; ++i) {185
const recievedMessage = payload.messages[i];186
const record = loggedMessages[recievedMessage.id];187
188
if (record == null || record.message == null) continue;189
190
if (record.message.editHistory!.length !== 0) {191
payload.messages[i].editHistory = record.message.editHistory;192
}193
194
}195
196
const fetchUser = (id: string) => UserStore.getUser(id) || payload.messages.find(e => e.author.id === id);197
198
for (let i = 0, len = recordIDs.length; i < len; i++) {199
const id = recordIDs[i];200
if (!loggedMessages[id]) continue;201
const { message } = loggedMessages[id] as { message: LoggedMessageJSON; };202
203
for (let j = 0, len2 = message.mentions.length; j < len2; j++) {204
const user = message.mentions[j];205
const cachedUser = fetchUser((user as any).id || user);206
if (cachedUser) (message.mentions[j] as any) = cleanupUserObject(cachedUser);207
}208
209
const author = fetchUser(message.author.id);210
if (!author) continue;211
(message.author as any) = cleanupUserObject(author);212
}213
214
reAddDeletedMessages(payload.messages, deletedMessages, !payload.hasMoreAfter && !payload.isBefore, !payload.hasMoreBefore && !payload.isAfter);215
}216
217
export const settings = definePluginSettings({218
saveMessages: {219
default: true,220
type: OptionType.BOOLEAN,221
description: "Wether to save the deleted and edited messages.",222
},223
224
saveImages: {225
type: OptionType.BOOLEAN,226
description: "Save deleted messages",227
default: false228
},229
230
sortNewest: {231
default: true,232
type: OptionType.BOOLEAN,233
description: "Sort logs by newest.",234
},235
236
cacheMessagesFromServers: {237
default: false,238
type: OptionType.BOOLEAN,239
description: "Usually message logger only logs from whitelisted ids and dms, enabling this would mean it would log messages from all servers as well. Note that this may cause the cache to exceed its limit, resulting in some messages being missed. If you are in a lot of servers, this may significantly increase the chances of messages being logged, which can result in a large message record and the inclusion of irrelevant messages.",240
},241
242
autoCheckForUpdates: {243
default: true,244
type: OptionType.BOOLEAN,245
description: "Automatically check for updates on startup.",246
},247
248
ignoreBots: {249
type: OptionType.BOOLEAN,250
description: "Whether to ignore messages by bots",251
default: false,252
onChange() {253
// we will be handling the ignoreBots now (enabled or not) so the original messageLogger shouldnt254
Settings.plugins.MessageLogger.ignoreBots = false;255
}256
},257
258
ignoreSelf: {259
type: OptionType.BOOLEAN,260
description: "Whether to ignore messages by yourself",261
default: false,262
onChange() {263
Settings.plugins.MessageLogger.ignoreSelf = false;264
}265
},266
267
ignoreMutedGuilds: {268
default: false,269
type: OptionType.BOOLEAN,270
description: "Messages in muted guilds will not be logged. Whitelisted users/channels in muted guilds will still be logged."271
},272
273
ignoreMutedCategories: {274
default: false,275
type: OptionType.BOOLEAN,276
description: "Messages in channels belonging to muted categories will not be logged. Whitelisted users/channels in muted guilds will still be logged."277
},278
279
ignoreMutedChannels: {280
default: false,281
type: OptionType.BOOLEAN,282
description: "Messages in muted channels will not be logged. Whitelisted users/channels in muted guilds will still be logged."283
},284
285
alwaysLogDirectMessages: {286
default: true,287
type: OptionType.BOOLEAN,288
description: "Always log DMs",289
},290
291
alwaysLogCurrentChannel: {292
default: true,293
type: OptionType.BOOLEAN,294
description: "Always log current selected channel. Blacklisted channels/users will still be ignored.",295
},296
297
permanentlyRemoveLogByDefault: {298
default: false,299
type: OptionType.BOOLEAN,300
description: "FiveCord039;s base MessageLogger remove log button wiil delete logs permanently",301
},302
303
hideMessageFromMessageLoggers: {304
default: false,305
type: OptionType.BOOLEAN,306
description: "When enabled, a context menu button will be added to messages to allow you to delete messages without them being logged by other loggers. Might not be safe, use at your own risk."307
},308
309
ShowLogsButton: {310
default: true,311
type: OptionType.BOOLEAN,312
description: "Toggle to whenever show the toolbox or not",313
restartNeeded: true,314
},315
316
hideMessageFromMessageLoggersDeletedMessage: {317
default: "redacted eh",318
type: OptionType.STRING,319
description: "The message content to replace the message with when using the hide message from message loggers feature.",320
},321
322
messageLimit: {323
default: 200,324
type: OptionType.NUMBER,325
description: "Maximum number of messages to save. Older messages are deleted when the limit is reached. 0 means there is no limit"326
},327
328
imagesLimit: {329
default: 100,330
type: OptionType.NUMBER,331
description: "Maximum number of images to save. Older images are deleted when the limit is reached. 0 means there is no limit"332
},333
334
cacheLimit: {335
default: 1000,336
type: OptionType.NUMBER,337
description: "Maximum number of messages to store in the cache. Older messages are deleted when the limit is reached. This helps reduce memory usage and improve performance. 0 means there is no limit",338
},339
340
whitelistedIds: {341
default: "",342
type: OptionType.STRING,343
description: "Whitelisted server, channel, or user IDs."344
},345
346
blacklistedIds: {347
default: "",348
type: OptionType.STRING,349
description: "Blacklisted server, channel, or user IDs."350
},351
352
imageCacheDir: {353
type: OptionType.COMPONENT,354
description: "Select saved images directory",355
component: ErrorBoundary.wrap(ImageCacheDir) as any356
},357
358
logsDir: {359
type: OptionType.COMPONENT,360
description: "Select logs directory",361
component: ErrorBoundary.wrap(LogsDir) as any362
},363
364
exportLogs: {365
type: OptionType.COMPONENT,366
description: "Export Logs From IndexedDB",367
component: () =>368
<Button onClick={downloadLoggedMessages}>369
Export Logs370
</Button>371
},372
openLogs: {373
type: OptionType.COMPONENT,374
description: "Open Logs",375
component: () =>376
<Button onClick={() => openLogModal()}>377
Open Logs378
</Button>379
},380
openImageCacheFolder: {381
type: OptionType.COMPONENT,382
description: "Opens the image cache directory",383
component: () =>384
<Button385
disabled={386
IS_WEB387
|| settings.store.imageCacheDir == null388
|| settings.store.imageCacheDir === DEFAULT_IMAGE_CACHE_DIR389
}390
onClick={() => Native.showItemInFolder(settings.store.imageCacheDir)}391
>392
Open Image Cache Folder393
</Button>394
},395
396
clearLogs: {397
type: OptionType.COMPONENT,398
description: "Clear Logs",399
component: () =>400
<Button401
color={Button.Colors.RED}402
onClick={() => Alerts.show({403
title: "Clear Logs",404
body: "Are you sure you want to clear all logs?",405
confirmColor: Button.Colors.RED,406
confirmText: "Clear",407
cancelText: "Cancel",408
onConfirm: () => {409
LoggedMessageManager.clearLogs();410
},411
})}412
>413
Clear Logs414
</Button>415
},416
417
});418
419
const idFunctions = {420
Server: props => props?.guild?.id,421
User: props => props?.message?.author?.id || props?.user?.id,422
Channel: props => props.message?.channel_id || props.channel?.id423
} as const;424
425
type idKeys = keyof typeof idFunctions;426
427
function renderListOption(listType: ListType, IdType: idKeys, props: any) {428
const id = idFunctions[IdType](props);429
if (!id) return null;430
431
const isBlocked = settings.store[listType].includes(id);432
const oppositeListType = listType === "blacklistedIds" ? "whitelistedIds" : "blacklistedIds";433
const isOppositeBlocked = settings.store[oppositeListType].includes(id);434
const list = listType === "blacklistedIds" ? "Blacklist" : "Whitelist";435
436
const addToList = () => addToXAndRemoveFromOpposite(listType, id);437
const removeFromList = () => removeFromX(listType, id);438
439
return (440
<Menu.MenuItem441
id={`${listType}-${IdType}-${id}`}442
label={443
isOppositeBlocked444
? `Move ${IdType} to ${list}`445
: isBlocked ? `Remove ${IdType} From ${list}` : `${list} ${IdType}`446
}447
action={isBlocked ? removeFromList : addToList}448
/>449
);450
}451
452
function renderOpenLogs(idType: idKeys, props: any) {453
const id = idFunctions[idType](props);454
if (!id) return null;455
456
return (457
<Menu.MenuItem458
id={`open-logs-for-${idType.toLowerCase()}`}459
label={`Open Logs For ${idType}`}460
action={() => openLogModal(`${idType.toLowerCase()}:${id}`)}461
/>462
);463
}464
465
const contextMenuPath: NavContextMenuPatchCallback = (children, props) => {466
if (!props) return;467
468
if (!children.some(child => child?.props?.id === "message-logger")) {469
children.push(470
<Menu.MenuSeparator />,471
<Menu.MenuItem472
id="message-logger"473
label="Message Logger"474
>475
476
<Menu.MenuItem477
id="open-logs"478
label="Open Logs"479
action={() => openLogModal()}480
/>481
482
{Object.keys(idFunctions).map(IdType => renderOpenLogs(IdType as idKeys, props))}483
484
<Menu.MenuSeparator />485
486
{Object.keys(idFunctions).map(IdType => (487
<React.Fragment key={IdType}>488
{renderListOption("blacklistedIds", IdType as idKeys, props)}489
{renderListOption("whitelistedIds", IdType as idKeys, props)}490
</React.Fragment>491
))}492
493
{494
props.navId === "message"495
&& (props.message?.deleted || props.message?.editHistory?.length > 0)496
&& (497
<>498
<Menu.MenuSeparator />499
<Menu.MenuItem500
id="remove-message"501
label={props.message?.deleted ? "Remove Message (Permanent)" : "Remove Message History (Permanent)"}502
color="danger"503
action={() =>504
removeLog(props.message.id)505
.then(() => {506
if (props.message.deleted) {507
FluxDispatcher.dispatch({508
type: "MESSAGE_DELETE",509
channelId: props.message.channel_id,510
id: props.message.id,511
mlDeleted: true512
});513
} else {514
props.message.editHistory = [];515
}516
}).catch(() => Toasts.show({517
type: Toasts.Type.FAILURE,518
message: "Failed to remove message",519
id: Toasts.genId()520
}))521
522
}523
/>524
</>525
)526
}527
528
{529
settings.store.hideMessageFromMessageLoggers530
&& props.navId === "message"531
&& props.message?.author?.id === UserStore.getCurrentUser().id532
&& props.message?.deleted === false533
&& (534
<>535
<Menu.MenuSeparator />536
<Menu.MenuItem537
id="hide-from-message-loggers"538
label="Delete Message (Hide From Message Loggers)"539
color="danger"540
541
action={async () => {542
await MessageActions.deleteMessage(props.message.channel_id, props.message.id);543
MessageActions._sendMessage(props.message.channel_id, {544
"content": settings.store.hideMessageFromMessageLoggersDeletedMessage,545
"tts": false,546
"invalidEmojis": [],547
"validNonShortcutEmojis": []548
}, { nonce: props.message.id });549
}}550
551
/>552
</>553
)554
}555
556
</Menu.MenuItem>557
);558
}559
};560
561
export default definePlugin({562
name: "MLEnhanced",563
authors: [Devs.FiveCord],564
description: "G039;day",565
dependencies: ["MessageLogger"],566
contextMenus: {567
"message": contextMenuPath,568
"channel-context": contextMenuPath,569
"user-context": contextMenuPath,570
"guild-context": contextMenuPath,571
"gdm-context": contextMenuPath572
},573
patches: [574
{575
find: 039;"MessageStore"039;,576
replacement: {577
match: /LOAD_MESSAGES_SUCCESS:function\(\i\){/,578
replace: "$&$self.messageLoadSuccess(arguments[0]);"579
}580
},581
582
{583
find: "THREAD_STARTER_MESSAGE?null===",584
replacement: {585
match: /(attachments: \i\(.{1,500})deleted:.{1,50},editHistory:.{1,30},/,586
replace: "$1deleted: $self.getDeleted(...arguments),editHistory: $self.getEdited(...arguments),"587
}588
},589
590
{591
find: "toolbar:function",592
predicate: () => settings.store.ShowLogsButton,593
replacement: {594
match: /(function \i\(\i\){)(.{1,200}toolbar.{1,100}mobileToolbar)/,595
replace: "$1$self.addIconToToolBar(arguments[0]);$2"596
}597
},598
599
{600
find: ",guildId:void 0}),childrenMessageContent",601
replacement: {602
match: /(cozyMessage.{1,50},)childrenHeader:/,603
replace: "$1childrenAccessories:arguments[0].childrenAccessories || null,childrenHeader:"604
}605
},606
607
// https://regex101.com/r/TMV1vY/1608
{609
find: ".removeAttachmentHoverButton",610
replacement: {611
match: /(\i=(\i)=>{)(.{1,250}isSingleMosaicItem)/,612
replace: "$1 let forceUpdate=FiveCord.Util.useForceUpdater();$self.patchAttachments($2,forceUpdate);$3"613
}614
},615
616
{617
find: "handleImageLoad=",618
replacement: {619
match: /(render\(\){)(.{1,100}zoomThumbnailPlaceholder)/,620
replace: "$1$self.checkImage(this);$2"621
}622
},623
624
// dont fetch messages for channels in modal625
{626
find: "Using PollReferenceMessageContext without",627
replacement: {628
match: /\i\.(?:default\.)?focusMessage\(/,629
replace: "!(arguments[0]?.message?.deleted || arguments[0]?.message?.editHistory?.length > 0) && $&"630
}631
},632
633
// only check for expired attachments if the message is not deleted634
{635
find: "\"/ephemeral-attachments/\"",636
replacement: {637
match: /\i\.attachments\.some\(\i\)\|\|\i\.embeds\.some/,638
replace: "!arguments[0].deleted && $&"639
}640
}641
],642
settings,643
644
toolboxActions: {645
"Message Logger"() {646
openLogModal();647
}648
},649
650
addIconToToolBar(e: { toolbar: React.ReactNode[] | React.ReactNode; }) {651
if (Array.isArray(e.toolbar))652
return e.toolbar.push(653
<ErrorBoundary noop={true}>654
<OpenLogsButton />655
</ErrorBoundary>656
);657
658
e.toolbar = [659
<ErrorBoundary noop={true}>660
<OpenLogsButton />661
</ErrorBoundary>,662
e.toolbar,663
];664
},665
666
messageLoadSuccess,667
store: MessageLoggerStore,668
openLogModal,669
doesMatch,670
LoggedMessageManager,671
ImageManager,672
imageUtils,673
674
isDeletedMessage: (id: string) => loggedMessages.deletedMessages[id] != null,675
676
getDeleted(m1, m2) {677
const deleted = m2?.deleted;678
if (deleted == null && m1?.deleted != null) return m1.deleted;679
return deleted;680
},681
682
getEdited(m1, m2) {683
const editHistory = m2?.editHistory;684
if (editHistory == null && m1?.editHistory != null && m1.editHistory.length > 0)685
return m1.editHistory.map(mapEditHistory);686
return editHistory;687
},688
689
attachments: new Map<string, LoggedAttachment>(),690
patchAttachments(props: { attachment: LoggedAttachment, message: LoggedMessage; }, forceUpdate: () => void) {691
const { attachment, message } = props;692
if (!message.deleted || !LoggedMessageManager.hasMessageInLogs(message.id))693
return; class="ts-cmt">// Flogger.log("ignoring", message.id);694
695
if (this.attachments.has(attachment.id))696
return props.attachment = this.attachments.get(attachment.id)!; class="ts-cmt">// Flogger.log("blobUrl already exists");697
698
imageUtils.getAttachmentBlobUrl(attachment).then((blobUrl: string | null) => {699
if (blobUrl == null) {700
Flogger.error("image not found. for message.id =", message.id, blobUrl);701
return;702
}703
Flogger.log("Got blob url for message.id =", message.id, blobUrl);704
// we need to copy because changing this will change the attachment for the message in the logs705
const attachmentCopy = { ...attachment };706
707
attachmentCopy.oldUrl = attachment.url;708
709
const finalBlobUrl = blobUrl + "#";710
attachmentCopy.blobUrl = finalBlobUrl;711
attachmentCopy.url = finalBlobUrl;712
attachmentCopy.proxy_url = finalBlobUrl;713
this.attachments.set(attachment.id, attachmentCopy);714
forceUpdate();715
});716
717
},718
719
async checkImage(instance: any) {720
if (!instance.props.isBad && instance.state?.readyState !== "READY" && instance.props?.src?.startsWith("blob:")) {721
if (await doesBlobUrlExist(instance.props.src)) {722
Flogger.log("image exists", instance.props.src);723
return instance.setState(e => ({ ...e, readyState: "READY" }));724
}725
726
instance.props.isBad = true;727
}728
},729
730
flux: {731
"MESSAGE_DELETE": messageDeleteHandler,732
"MESSAGE_DELETE_BULK": messageDeleteBulkHandler,733
"MESSAGE_UPDATE": messageUpdateHandler,734
"MESSAGE_CREATE": messageCreateHandler735
},736
737
async start() {738
Native.init();739
740
const { imageCacheDir, logsDir } = await Native.getSettings();741
settings.store.imageCacheDir = imageCacheDir;742
settings.store.logsDir = logsDir;743
}744
});745