Plugin

MLEnhanced

G'day

index.tsx
Download

Source

src/plugins/messageLoggerEnhanced/index.tsx
1/*
2 * FiveCord — a Discord client mod
3 * Copyright (c) 2025 FiveCord
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7export const VERSION = "3.0.0";
8
9export const Native = getNative();
10
11import "./styles.css";
12
13import { NavContextMenuPatchCallback } from "@api/ContextMenu";
14import { definePluginSettings, Settings } from "@api/Settings";
15import ErrorBoundary from "@components/ErrorBoundary";
16import { Devs } from "@utils/constants";
17import { Logger } from "@utils/Logger";
18import definePlugin, { OptionType } from "@utils/types";
19import { findByPropsLazy } from "@webpack";
20import { Alerts, Button, FluxDispatcher, Menu, MessageActions, MessageStore, React, Toasts, UserStore } from "@webpack/common";
21
22import { OpenLogsButton } from "./components/LogsButton";
23import { openLogModal } from "./components/LogsModal";
24import { ImageCacheDir, LogsDir } from "./components/settings/FolderSelectInput";
25import * as LoggedMessageManager from "./LoggedMessageManager";
26import { addMessage, loggedMessages, MessageLoggerStore, removeLog } from "./LoggedMessageManager";
27import { LoadMessagePayload, LoggedAttachment, LoggedMessage, LoggedMessageJSON, MessageCreatePayload, MessageDeleteBulkPayload, MessageDeletePayload, MessageUpdatePayload } from "./types";
28import { addToXAndRemoveFromOpposite, cleanUpCachedMessage, cleanupUserObject, doesBlobUrlExist, getNative, isGhostPinged, ListType, mapEditHistory, reAddDeletedMessages, removeFromX } from "./utils";
29import { DEFAULT_IMAGE_CACHE_DIR } from "./utils/constants";
30import { shouldIgnore } from "./utils/index";
31import { LimitedMap } from "./utils/LimitedMap";
32import { doesMatch } from "./utils/parseQuery";
33import * as imageUtils from "./utils/saveImage";
34import * as ImageManager from "./utils/saveImage/ImageManager";
35import { downloadLoggedMessages } from "./utils/settingsUtils";
36
37
38export const Flogger = new Logger("MessageLoggerEnhanced", "#f26c6c");
39
40export const cacheSentMessages = new LimitedMap<string, LoggedMessageJSON>();
41
42const cacheThing = findByPropsLazy("commit", "getOrCreate");
43
44
45const handledMessageIds = new Set();
46async 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 message
66 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).ourCache
81 })
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: true
89 });
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
102async function messageDeleteBulkHandler({ channelId, guildId, ids }: MessageDeleteBulkPayload) {
103 // is this bad? idk man
104 for (const id of ids) {
105 await messageDeleteHandler({ type: "MESSAGE_DELETE", channelId, guildId, id, isBulk: true });
106 }
107 await LoggedMessageManager.saveLoggedMessages();
108}
109
110async 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 ?? false
121 })
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 = MessageStore
133 .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 null
137 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
160function messageCreateHandler(payload: MessageCreatePayload) {
161 // we do this here because cache is limited and to save memory
162 if (!settings.store.cacheMessagesFromServers && payload.guildId != null) {
163 const ids = [payload.channelId, payload.message?.author?.id, payload.guildId];
164 const isWhitelisted =
165 settings.store.whitelistedIds
166 .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 mlv2
178function 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
217export 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: false
228 },
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 shouldnt
254 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: "FiveCord&#039;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 any
356 },
357
358 logsDir: {
359 type: OptionType.COMPONENT,
360 description: "Select logs directory",
361 component: ErrorBoundary.wrap(LogsDir) as any
362 },
363
364 exportLogs: {
365 type: OptionType.COMPONENT,
366 description: "Export Logs From IndexedDB",
367 component: () =>
368 <Button onClick={downloadLoggedMessages}>
369 Export Logs
370 </Button>
371 },
372 openLogs: {
373 type: OptionType.COMPONENT,
374 description: "Open Logs",
375 component: () =>
376 <Button onClick={() => openLogModal()}>
377 Open Logs
378 </Button>
379 },
380 openImageCacheFolder: {
381 type: OptionType.COMPONENT,
382 description: "Opens the image cache directory",
383 component: () =>
384 <Button
385 disabled={
386 IS_WEB
387 || settings.store.imageCacheDir == null
388 || settings.store.imageCacheDir === DEFAULT_IMAGE_CACHE_DIR
389 }
390 onClick={() => Native.showItemInFolder(settings.store.imageCacheDir)}
391 >
392 Open Image Cache Folder
393 </Button>
394 },
395
396 clearLogs: {
397 type: OptionType.COMPONENT,
398 description: "Clear Logs",
399 component: () =>
400 <Button
401 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 Logs
414 </Button>
415 },
416
417});
418
419const 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?.id
423} as const;
424
425type idKeys = keyof typeof idFunctions;
426
427function 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.MenuItem
441 id={`${listType}-${IdType}-${id}`}
442 label={
443 isOppositeBlocked
444 ? `Move ${IdType} to ${list}`
445 : isBlocked ? `Remove ${IdType} From ${list}` : `${list} ${IdType}`
446 }
447 action={isBlocked ? removeFromList : addToList}
448 />
449 );
450}
451
452function renderOpenLogs(idType: idKeys, props: any) {
453 const id = idFunctions[idType](props);
454 if (!id) return null;
455
456 return (
457 <Menu.MenuItem
458 id={`open-logs-for-${idType.toLowerCase()}`}
459 label={`Open Logs For ${idType}`}
460 action={() => openLogModal(`${idType.toLowerCase()}:${id}`)}
461 />
462 );
463}
464
465const 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.MenuItem
472 id="message-logger"
473 label="Message Logger"
474 >
475
476 <Menu.MenuItem
477 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.MenuItem
500 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: true
512 });
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.hideMessageFromMessageLoggers
530 && props.navId === "message"
531 && props.message?.author?.id === UserStore.getCurrentUser().id
532 && props.message?.deleted === false
533 && (
534 <>
535 <Menu.MenuSeparator />
536 <Menu.MenuItem
537 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
561export default definePlugin({
562 name: "MLEnhanced",
563 authors: [Devs.FiveCord],
564 description: "G&#039;day",
565 dependencies: ["MessageLogger"],
566 contextMenus: {
567 "message": contextMenuPath,
568 "channel-context": contextMenuPath,
569 "user-context": contextMenuPath,
570 "guild-context": contextMenuPath,
571 "gdm-context": contextMenuPath
572 },
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/1
608 {
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 modal
625 {
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 deleted
634 {
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 logs
705 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": messageCreateHandler
735 },
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