From bcb36541f6d9a704e37b30bb81cf5081ba1694ba Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:54:49 -0500 Subject: [PATCH] MessageLoggerEnhanced: Update --- package.json | 4 +- pnpm-lock.yaml | 45 ++++ .../components/FolderSelectInput.tsx | 14 +- .../components/LogsModal.tsx | 40 ++-- .../messageLoggerEnhanced/components/hooks.ts | 4 +- .../messageLoggerEnhanced/db.ts | 24 ++ .../messageLoggerEnhanced/index.tsx | 48 ++-- .../messageLoggerEnhanced/native/export.ts | 57 +++++ .../messageLoggerEnhanced/native/import.ts | 54 +++++ .../messageLoggerEnhanced/native/index.ts | 7 +- .../messageLoggerEnhanced/native/utils.ts | 4 - .../messageLoggerEnhanced/settings.tsx | 64 +++++- .../messageLoggerEnhanced/styles.css | 23 +- .../messageLoggerEnhanced/types.ts | 18 +- .../messageLoggerEnhanced/utils/LimitedMap.ts | 8 +- .../messageLoggerEnhanced/utils/cleanUp.ts | 4 +- .../utils/contextMenu.tsx | 100 +++++---- .../messageLoggerEnhanced/utils/index.ts | 12 +- .../messageLoggerEnhanced/utils/misc.ts | 10 +- .../messageLoggerEnhanced/utils/parseQuery.ts | 2 +- .../utils/saveImage/ImageManager.ts | 7 +- .../utils/saveImage/index.ts | 6 +- .../utils/settingsUtils.ts | 212 ++++++++++++++---- src/equicordplugins/messagePeek/style.css | 7 +- .../_api/memberListDecorators/index.tsx | 14 ++ .../_api/memberListDecorators/style.css | 7 + src/plugins/showMeYourName/index.tsx | 4 +- src/utils/misc.ts | 4 +- 28 files changed, 591 insertions(+), 212 deletions(-) create mode 100644 src/equicordplugins/messageLoggerEnhanced/native/export.ts create mode 100644 src/equicordplugins/messageLoggerEnhanced/native/import.ts diff --git a/package.json b/package.json index 68a44e4c1..e1d7f371e 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@ffmpeg/ffmpeg": "^0.12.10", "@ffmpeg/util": "^0.12.1", "@intrnl/xxhash64": "^0.1.2", + "@streamparser/json": "^0.0.22", "@types/less": "^3.0.8", "@types/stylus": "^0.48.42", "@types/tinycolor2": "^1.4.6", @@ -53,6 +54,7 @@ "jsqr": "1.4.0", "monaco-editor": "^0.54.0", "nanoid": "^5.1.6", + "native-file-system-adapter": "^3.0.1", "usercss-meta": "^0.12.0", "virtual-merge": "^1.0.1" }, @@ -92,7 +94,7 @@ "typescript-transform-paths": "^3.5.5", "zip-local": "^0.3.5" }, - "packageManager": "pnpm@10.26.2", + "packageManager": "pnpm@10.29.2", "pnpm": { "patchedDependencies": { "@types/less@3.0.8": "patches/@types__less@3.0.8.patch" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24393b75a..654192c0b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,6 +25,9 @@ importers: '@intrnl/xxhash64': specifier: ^0.1.2 version: 0.1.2 + '@streamparser/json': + specifier: ^0.0.22 + version: 0.0.22 '@types/less': specifier: ^3.0.8 version: 3.0.8(patch_hash=641e6c93bb737bac7fc283416857bd095cd85bcbcba63becb7a8bbcc78f73076) @@ -58,6 +61,9 @@ importers: nanoid: specifier: ^5.1.6 version: 5.1.6 + native-file-system-adapter: + specifier: ^3.0.1 + version: 3.0.1 usercss-meta: specifier: ^0.12.0 version: 0.12.0 @@ -712,6 +718,9 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@streamparser/json@0.0.22': + resolution: {integrity: sha512-b6gTSBjJ8G8SuO3Gbbj+zXbVx8NSs1EbpbMKpzGLWMdkR+98McH9bEjSz3+0mPJf68c5nxa3CrJHp5EQNXM6zQ==} + '@stylistic/eslint-plugin@5.6.0': resolution: {integrity: sha512-owEc4B8ME+O/xyZOkLVyLqPMsUgJXIM4XzCm5Vt3WvRXpyoOfYxgA+JkEiFqXPCI8+Nc2BzAT+KGAK7QleGs8Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1445,6 +1454,10 @@ packages: fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -1959,6 +1972,10 @@ packages: engines: {node: ^18 || >=20} hasBin: true + native-file-system-adapter@3.0.1: + resolution: {integrity: sha512-ocuhsYk2SY0906LPc3QIMW+rCV3MdhqGiy7wV5Bf0e8/5TsMjDdyIwhNiVPiKxzTJLDrLT6h8BoV9ERfJscKhw==} + engines: {node: '>=14.8.0'} + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -1969,6 +1986,11 @@ packages: no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -2143,6 +2165,7 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. + (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qified@0.5.2: @@ -2534,6 +2557,10 @@ packages: vscode-textmate@5.2.0: resolution: {integrity: sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ==} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + webdriver-bidi-protocol@0.3.8: resolution: {integrity: sha512-21Yi2GhGntMc671vNBCjiAeEVknXjVRoyu+k+9xOMShu+ZQfpGQwnBqbNz/Sv4GXZ6JmutlPAi2nIJcrymAWuQ==} @@ -2968,6 +2995,8 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@streamparser/json@0.0.22': {} + '@stylistic/eslint-plugin@5.6.0(eslint@9.39.1)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) @@ -3908,6 +3937,12 @@ snapshots: dependencies: pend: 1.2.0 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + optional: true + fflate@0.8.2: {} file-entry-cache@10.1.4: @@ -4421,6 +4456,10 @@ snapshots: nanoid@5.1.6: {} + native-file-system-adapter@3.0.1: + optionalDependencies: + fetch-blob: 3.2.0 + natural-compare@1.4.0: {} netmask@2.0.2: {} @@ -4430,6 +4469,9 @@ snapshots: lower-case: 2.0.2 tslib: 2.6.2 + node-domexception@1.0.0: + optional: true + normalize-path@3.0.0: {} object-assign@4.1.1: {} @@ -5134,6 +5176,9 @@ snapshots: vscode-textmate@5.2.0: {} + web-streams-polyfill@3.3.3: + optional: true + webdriver-bidi-protocol@0.3.8: {} which-boxed-primitive@1.1.1: diff --git a/src/equicordplugins/messageLoggerEnhanced/components/FolderSelectInput.tsx b/src/equicordplugins/messageLoggerEnhanced/components/FolderSelectInput.tsx index c0d2ff937..0bcc6f641 100644 --- a/src/equicordplugins/messageLoggerEnhanced/components/FolderSelectInput.tsx +++ b/src/equicordplugins/messageLoggerEnhanced/components/FolderSelectInput.tsx @@ -16,15 +16,19 @@ * along with this program. If not, see . */ +import { Button } from "@components/Button"; import { Heading } from "@components/Heading"; -import { DEFAULT_IMAGE_CACHE_DIR } from "@equicordplugins/messageLoggerEnhanced/utils/constants"; import { classNameFactory } from "@utils/css"; import { copyWithToast } from "@utils/discord"; -import { Button, Toasts } from "@webpack/common"; +import { classes } from "@utils/misc"; +import { findCssClassesLazy } from "@webpack"; +import { Toasts } from "@webpack/common"; import { Native, settings } from ".."; +import { DEFAULT_IMAGE_CACHE_DIR } from "../utils/constants"; const cl = classNameFactory("folder-upload"); +const inputClasses = findCssClassesLazy("input", "inputWrapper", "editable") as Record; function createDirSelector(settingKey: "logsDir" | "imageCacheDir", successMessage: string) { return function DirSelector({ option }) { @@ -32,7 +36,7 @@ function createDirSelector(settingKey: "logsDir" | "imageCacheDir", successMessa return ( - {option.description} + {option.description} + copyWithToast(path)} className={cl("-input")}> {path == null || path === DEFAULT_IMAGE_CACHE_DIR ? "Choose Folder" : getDirName(path)} Browse diff --git a/src/equicordplugins/messageLoggerEnhanced/components/LogsModal.tsx b/src/equicordplugins/messageLoggerEnhanced/components/LogsModal.tsx index 5102ee9ce..916da9520 100644 --- a/src/equicordplugins/messageLoggerEnhanced/components/LogsModal.tsx +++ b/src/equicordplugins/messageLoggerEnhanced/components/LogsModal.tsx @@ -5,21 +5,23 @@ */ import { BaseText } from "@components/BaseText"; +import { Button } from "@components/Button"; import { Flex } from "@components/Flex"; import { InfoIcon } from "@components/Icons"; -import { clearMessagesIDB, DBMessageRecord, deleteMessageIDB, deleteMessagesBulkIDB } from "@equicordplugins/messageLoggerEnhanced/db"; -import { settings } from "@equicordplugins/messageLoggerEnhanced/index"; -import { LoggedMessage, LoggedMessageJSON } from "@equicordplugins/messageLoggerEnhanced/types"; -import { messageJsonToMessageClass } from "@equicordplugins/messageLoggerEnhanced/utils"; -import { importLogs } from "@equicordplugins/messageLoggerEnhanced/utils/settingsUtils"; +import { Link } from "@components/Link"; import { classNameFactory } from "@utils/css"; import { copyWithToast, openUserProfile } from "@utils/discord"; import { closeAllModals, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; import { LazyComponent } from "@utils/react"; -import { User } from "@vencord/discord-types"; +import { type User } from "@vencord/discord-types"; import { find, findByCode, findByCodeLazy } from "@webpack"; -import { Alerts, Button, ChannelStore, ContextMenuApi, FluxDispatcher, GuildStore, Menu, NavigationRouter, React, TabBar, TextInput, Tooltip, useMemo, useRef, useState } from "@webpack/common"; +import { Alerts, ChannelStore, ContextMenuApi, FluxDispatcher, GuildStore, Menu, NavigationRouter, React, TabBar, TextInput, Tooltip, useMemo, useRef, useState } from "@webpack/common"; +import { clearMessagesIDB, DBMessageRecord, deleteMessageIDB, deleteMessagesBulkIDB } from "../db"; +import { settings } from "../index"; +import { LoggedMessage, LoggedMessageJSON } from "../types"; +import { messageJsonToMessageClass } from "../utils"; +import { importLogs } from "../utils/settingsUtils"; import { useMessages } from "./hooks"; export interface MessagePreviewProps { @@ -89,7 +91,6 @@ export function LogsModal({ modalProps, initalQuery }: Props) { setCurrentTab(e); setNumDisplayedMessages(settings.store.messagesToDisplayAtOnceInLogs); contentRef.current?.firstElementChild?.scrollTo(0, 0); - // forceUpdate(); }} > } - + Alerts.show({ title: "Clear Logs", body: "Are you sure you want to clear all the logs", confirmText: "Clear", - confirmColor: Button.Colors.RED, + // @ts-expect-error not typed + confirmVariant: "critical-primary", cancelText: "Cancel", onConfirm: async () => { await clearMessagesIDB(); @@ -158,13 +160,14 @@ export function LogsModal({ modalProps, initalQuery }: Props) { Alerts.show({ title: "Clear Logs", body: `Are you sure you want to clear ${messages.length} logs`, confirmText: "Clear", - confirmColor: Button.Colors.RED, + // @ts-expect-error not typed + confirmVariant: "critical-primary", cancelText: "Cancel", onConfirm: async () => { await deleteMessagesBulkIDB(messages.map(e => e.message_id)); @@ -174,9 +177,8 @@ export function LogsModal({ modalProps, initalQuery }: Props) { > Clear Visible Logs - { setSortNewest(e => { const val = !e; @@ -187,7 +189,7 @@ export function LogsModal({ modalProps, initalQuery }: Props) { }} > Sort {sortNewest ? "Oldest First" : "Newest First"} - + ); @@ -221,7 +223,7 @@ function LogsContent({ visibleMessages, canLoadMore, sortNewest, tab, reset, han canLoadMore && handleLoadMore()} + size="small" onClick={() => handleLoadMore()} > Load More @@ -302,8 +304,6 @@ interface LMessageProps { function LMessage({ log, isGroupStart, reset, }: LMessageProps) { const message = useMemo(() => messageJsonToMessageClass(log), [log]); - // console.log(message); - if (!message) return null; const channel = ChannelStore.getChannel(message?.channel_id); diff --git a/src/equicordplugins/messageLoggerEnhanced/components/hooks.ts b/src/equicordplugins/messageLoggerEnhanced/components/hooks.ts index 23315ca83..a60b472f9 100644 --- a/src/equicordplugins/messageLoggerEnhanced/components/hooks.ts +++ b/src/equicordplugins/messageLoggerEnhanced/components/hooks.ts @@ -4,10 +4,10 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { countMessagesByStatusIDB, countMessagesIDB, DBMessageRecord, DBMessageStatus, getDateStortedMessagesByStatusIDB } from "@equicordplugins/messageLoggerEnhanced/db"; -import { doesMatch, tokenizeQuery } from "@equicordplugins/messageLoggerEnhanced/utils/parseQuery"; import { useEffect, useState } from "@webpack/common"; +import { countMessagesByStatusIDB, countMessagesIDB, DBMessageRecord, DBMessageStatus, getDateStortedMessagesByStatusIDB } from "../db"; +import { doesMatch, tokenizeQuery } from "../utils/parseQuery"; import { LogTabs } from "./LogsModal"; function useDebouncedValue(value: T, delay: number): T { diff --git a/src/equicordplugins/messageLoggerEnhanced/db.ts b/src/equicordplugins/messageLoggerEnhanced/db.ts index 21dcaa3a8..f0d7e03d3 100644 --- a/src/equicordplugins/messageLoggerEnhanced/db.ts +++ b/src/equicordplugins/messageLoggerEnhanced/db.ts @@ -110,6 +110,30 @@ export async function getOldestMessagesIDB(limit: number) { return cacheRecords(await db.getAllFromIndex("messages", "by_timestamp", undefined, limit)); } +export async function* iterateAllMessagesIDB(batchSize = 100) { + let lastId: string | undefined; + while (true) { + const batch: DBMessageRecord[] = []; + // new transaction for each batch to avoid timeouts during yield + const tx = db.transaction("messages"); + const range = lastId ? IDBKeyRange.lowerBound(lastId, true) : undefined; + let cursor = await tx.store.openCursor(range); + + while (cursor && batch.length < batchSize) { + batch.push(cursor.value); + cursor = await cursor.continue(); + } + + if (batch.length === 0) break; + + lastId = batch[batch.length - 1].message_id; + + yield await cacheRecords(batch); + + if (batch.length < batchSize) break; + } +} + export async function getOlderThanTimestampIDB(timestamp: string) { const tx = db.transaction("messages", "readonly"); const { store } = tx; diff --git a/src/equicordplugins/messageLoggerEnhanced/index.tsx b/src/equicordplugins/messageLoggerEnhanced/index.tsx index 3fde1f61a..38788a629 100644 --- a/src/equicordplugins/messageLoggerEnhanced/index.tsx +++ b/src/equicordplugins/messageLoggerEnhanced/index.tsx @@ -40,17 +40,20 @@ let oldGetMessage: typeof MessageStore.getMessage; const handledMessageIds = new Set(); async function messageDeleteHandler(payload: MessageDeletePayload & { isBulk: boolean; }) { - if (payload.mlDeleted) return; + if (payload.mlDeleted) { + if (settings.store.permanentlyRemoveLogByDefault) + await idb.deleteMessageIDB(payload.id); + + return; + } if (handledMessageIds.has(payload.id)) { - // Flogger.warn("skipping duplicate message", payload.id); return; } try { handledMessageIds.add(payload.id); - // @ts-ignore let message: LoggedMessage | LoggedMessageJSON | null = oldGetMessage?.(payload.channelId, payload.id); if (message == null) { @@ -75,7 +78,6 @@ async function messageDeleteHandler(payload: MessageDeletePayload & { isBulk: bo webhookId: message?.webhookId }) ) { - // Flogger.log("IGNORING", message, payload); return FluxDispatcher.dispatch({ type: "MESSAGE_DELETE", channelId: payload.channelId, @@ -85,7 +87,6 @@ async function messageDeleteHandler(payload: MessageDeletePayload & { isBulk: bo } if (message == null || message.channel_id == null || !message.deleted) return; - // Flogger.log("ADDING MESSAGE (DELETED)", message); if (payload.isBulk) return message; @@ -138,7 +139,7 @@ async function messageUpdateHandler(payload: MessageUpdatePayload) { message.editHistory = []; cacheThing.commit(cache); } - return;// Flogger.log("this message has been ignored", payload); + return; } let message = oldGetMessage?.(payload.message.channel_id, payload.message.id) as LoggedMessage | LoggedMessageJSON | null; @@ -158,14 +159,12 @@ async function messageUpdateHandler(payload: MessageUpdatePayload) { ] }; - // @ts-ignore cacheSentMessages.set(`${payload.message.channel_id},${payload.message.id}`, message); } } if (message == null || message.channel_id == null || message.editHistory == null || message.editHistory.length === 0) return; - // Flogger.log("ADDING MESSAGE (EDITED)", message, payload); const currentChannelId = SelectedChannelStore.getChannelId(); await addMessage(message, idb.DBMessageStatus.EDITED, currentChannelId); } @@ -184,7 +183,6 @@ function messageCreateHandler(payload: MessageCreatePayload) { } cacheSentMessages.set(`${payload.message.channel_id},${payload.message.id}`, cleanUpCachedMessage(payload.message)); - // Flogger.log(`cached\nkey:${payload.message.channel_id},${payload.message.id}\nvalue:`, payload.message); } async function processMessageFetch(response: FetchMessagesResponse) { @@ -195,13 +193,14 @@ async function processMessageFetch(response: FetchMessagesResponse) { } const firstMessage = response.body[response.body.length - 1]; - // console.time("fetching messages from idb"); const messages = await idb.getMessagesByChannelAndAfterTimestampIDB(firstMessage.channel_id, firstMessage.timestamp); - // console.timeEnd("fetching messages from idb"); if (!messages.length) return; - const deletedMessages = messages.filter(m => m.status === idb.DBMessageStatus.DELETED); + const deletedMessages = messages.filter(m => + m.status === idb.DBMessageStatus.DELETED || + m.status === idb.DBMessageStatus.GHOST_PINGED + ); for (const recivedMessage of response.body) { const record = messages.find(m => m.message_id === recivedMessage.id); @@ -223,7 +222,7 @@ async function processMessageFetch(response: FetchMessagesResponse) { for (let j = 0, len2 = message.mentions.length; j < len2; j++) { const user = message.mentions[j]; - const cachedUser = fetchUser(user); + const cachedUser = fetchUser((user as any).id || user); if (cachedUser) (message.mentions[j] as any) = cleanupUserObject(cachedUser); } @@ -267,14 +266,14 @@ export default definePlugin({ replace: "deleted:$self.getDeleted(...arguments), editHistory:$self.getEdited(...arguments)," } }, + // MessagePreview component in LogsModal { - find: "childrenMessageContent:null", + find: "=!0,disableInteraction:", replacement: { - match: /(\i\]:!\i.{1,50},)childrenHeader:/, - replace: "$1childrenAccessories:arguments[0].childrenAccessories || null,childrenHeader:" + match: /childrenHeader:.{0,100}childrenMessageContent/, + replace: "childrenAccessories:arguments[0].childrenAccessories || null,$&" } }, - // https://regex101.com/r/S3IVGm/1 // fix vidoes failing because there are no thumbnails { find: ".handleImageLoad)", @@ -370,9 +369,20 @@ export default definePlugin({ // we have to do this because the original message logger fetches the message from the store now MessageStore.getMessage = (channelId: string, messageId: string) => { const MLMessage = idb.cachedMessages.get(messageId); - if (MLMessage) return messageJsonToMessageClass({ message: MLMessage }); + if (!MLMessage) + return this.oldGetMessage(channelId, messageId); - return this.oldGetMessage(channelId, messageId); + if (MLMessage.deleted) + return messageJsonToMessageClass({ message: MLMessage }); + + // update the edited message with the latest data + const latestMessage = this.oldGetMessage(channelId, messageId); + return messageJsonToMessageClass({ + message: { + ...MLMessage, + ...(latestMessage ?? {}), + } + }); }; Native.init(); diff --git a/src/equicordplugins/messageLoggerEnhanced/native/export.ts b/src/equicordplugins/messageLoggerEnhanced/native/export.ts new file mode 100644 index 000000000..2a881f1b2 --- /dev/null +++ b/src/equicordplugins/messageLoggerEnhanced/native/export.ts @@ -0,0 +1,57 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { randomUUID } from "node:crypto"; +import { createWriteStream, WriteStream } from "node:fs"; + +import { dialog, IpcMainInvokeEvent } from "electron"; + +const activeStreams = new Map(); + +export async function startNativeLogExport(_event: IpcMainInvokeEvent, filename: string) { + const { filePath, canceled } = await dialog.showSaveDialog({ + defaultPath: filename ?? "message-logger-logs-idb.json", + filters: [{ name: "JSON", extensions: ["json"] }] + }); + + if (canceled || !filePath) + throw new Error("No file path selected"); + + const stream = createWriteStream(filePath, { flags: "w", encoding: "utf-8" }); + + const streamId = randomUUID(); + activeStreams.set(streamId, stream); + + stream.on("error", err => { + console.error("Stream error", err); + activeStreams.delete(streamId); + }); + + return streamId; +} + +export async function writeNativeLogChunk(_event: IpcMainInvokeEvent, streamId: string, chunk: string) { + const stream = activeStreams.get(streamId); + if (!stream) throw new Error("Stream not found or closed"); + + // if returns false, buffer is full; wait for drain to prevent RAM spike + const canContinue = stream.write(chunk); + if (!canContinue) { + await new Promise(resolve => stream.once("drain", resolve)); + } +} + +export async function finishNativeLogExport(_event: IpcMainInvokeEvent, streamId: string) { + const stream = activeStreams.get(streamId); + if (!stream) return; + + return new Promise(resolve => { + stream.end(() => { + activeStreams.delete(streamId); + resolve(); + }); + }); +} diff --git a/src/equicordplugins/messageLoggerEnhanced/native/import.ts b/src/equicordplugins/messageLoggerEnhanced/native/import.ts new file mode 100644 index 000000000..9db8d9d26 --- /dev/null +++ b/src/equicordplugins/messageLoggerEnhanced/native/import.ts @@ -0,0 +1,54 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { randomUUID } from "node:crypto"; +import { FileHandle, open } from "node:fs/promises"; + +import { dialog, IpcMainInvokeEvent } from "electron"; + +const activeFiles = new Map(); + +export async function startNativeLogImport(_event: IpcMainInvokeEvent, defaultPath?: string) { + const res = await dialog.showOpenDialog({ + title: "Import Logs", + filters: [{ name: "Logs", extensions: ["json"] }], + properties: ["openFile"], + defaultPath + }); + const [path] = res.filePaths; + + if (!path) throw Error("No file selected"); + + const fileHandle = await open(path, "r"); + const fileId = randomUUID(); + activeFiles.set(fileId, fileHandle); + + return fileId; +} + +export async function readNativeLogChunk(_event: IpcMainInvokeEvent, fileId: string, size: number = 64 * 1024): Promise { + const fileHandle = activeFiles.get(fileId); + if (!fileHandle) return null; + + const buffer = Buffer.alloc(size); + const { bytesRead } = await fileHandle.read(buffer, 0, size); + + if (bytesRead === 0) { + await fileHandle.close(); + activeFiles.delete(fileId); + return null; + } + + return buffer.toString("utf-8", 0, bytesRead); +} + +export async function closeNativeLogImport(_event: IpcMainInvokeEvent, fileId: string) { + const fileHandle = activeFiles.get(fileId); + if (fileHandle) { + await fileHandle.close(); + activeFiles.delete(fileId); + } +} diff --git a/src/equicordplugins/messageLoggerEnhanced/native/index.ts b/src/equicordplugins/messageLoggerEnhanced/native/index.ts index cfde35f03..08c763cea 100644 --- a/src/equicordplugins/messageLoggerEnhanced/native/index.ts +++ b/src/equicordplugins/messageLoggerEnhanced/native/index.ts @@ -7,12 +7,15 @@ import { readdir, readFile, unlink, writeFile } from "node:fs/promises"; import path from "node:path"; -import { LoggedAttachment } from "@equicordplugins/messageLoggerEnhanced/types"; -import { LOGS_DATA_FILENAME } from "@equicordplugins/messageLoggerEnhanced/utils/constants"; import { DATA_DIR } from "@main/utils/constants"; import { dialog, IpcMainInvokeEvent, shell } from "electron"; import { getSettings, saveSettings } from "./settings"; +export * from "./export"; +export * from "./import"; + +import { LoggedAttachment } from "../types"; +import { LOGS_DATA_FILENAME } from "../utils/constants"; import { ensureDirectoryExists, getAttachmentIdFromFilename, sleep } from "./utils"; export { getSettings }; diff --git a/src/equicordplugins/messageLoggerEnhanced/native/utils.ts b/src/equicordplugins/messageLoggerEnhanced/native/utils.ts index e29bdca0c..691747f04 100644 --- a/src/equicordplugins/messageLoggerEnhanced/native/utils.ts +++ b/src/equicordplugins/messageLoggerEnhanced/native/utils.ts @@ -25,8 +25,4 @@ export function getAttachmentIdFromFilename(filename: string) { return path.parse(filename).name; } -export function getAttachmentExtFromFilename(filename: string) { - return path.parse(filename).ext; -} - export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/src/equicordplugins/messageLoggerEnhanced/settings.tsx b/src/equicordplugins/messageLoggerEnhanced/settings.tsx index fd54ebe2c..be5401ac5 100644 --- a/src/equicordplugins/messageLoggerEnhanced/settings.tsx +++ b/src/equicordplugins/messageLoggerEnhanced/settings.tsx @@ -5,9 +5,10 @@ */ import { definePluginSettings } from "@api/Settings"; +import { Button } from "@components/Button"; import ErrorBoundary from "@components/ErrorBoundary"; import { OptionType } from "@utils/types"; -import { Alerts, Button } from "@webpack/common"; +import { Alerts, useState } from "@webpack/common"; import { Settings } from "Vencord"; import { Native } from "."; @@ -18,6 +19,46 @@ import { blockedExts } from "./list"; import { DEFAULT_IMAGE_CACHE_DIR } from "./utils/constants"; import { exportLogs, importLogs } from "./utils/settingsUtils"; +function ImportLogsButton() { + const [loading, setLoading] = useState(false); + + return ( + { + setLoading(true); + try { + await importLogs(); + } finally { + setLoading(false); + } + }} + > + {loading ? "Importing..." : "Import Logs"} + + ); +} + +function ExportLogsButton() { + const [loading, setLoading] = useState(false); + + return ( + { + setLoading(true); + try { + await exportLogs(); + } finally { + setLoading(false); + } + }} + > + {loading ? "Exporting..." : "Export Logs"} + + ); +} + export const settings = definePluginSettings({ saveMessages: { default: true, @@ -98,6 +139,12 @@ export const settings = definePluginSettings({ description: "Always log current selected channel. Blacklisted channels/users will still be ignored.", }, + permanentlyRemoveLogByDefault: { + default: false, + type: OptionType.BOOLEAN, + description: "Vencord's base MessageLogger remove log button wiil delete logs permanently", + }, + hideMessageFromMessageLoggers: { default: false, type: OptionType.BOOLEAN, @@ -204,19 +251,13 @@ export const settings = definePluginSettings({ importLogs: { type: OptionType.COMPONENT, description: "Import Logs From File", - component: () => - - Import Logs - + component: ImportLogsButton }, exportLogs: { type: OptionType.COMPONENT, description: "Export Logs From IndexedDB", - component: () => - - Export Logs - + component: ExportLogsButton }, openLogs: { @@ -248,11 +289,12 @@ export const settings = definePluginSettings({ description: "Clear Logs", component: () => Alerts.show({ title: "Clear Logs", body: "Are you sure you want to clear all logs?", - confirmColor: Button.Colors.RED, + // @ts-expect-error not typed + confirmVariant: "critical-primary", confirmText: "Clear", cancelText: "Cancel", onConfirm: () => { diff --git a/src/equicordplugins/messageLoggerEnhanced/styles.css b/src/equicordplugins/messageLoggerEnhanced/styles.css index a14476188..5e508be4e 100644 --- a/src/equicordplugins/messageLoggerEnhanced/styles.css +++ b/src/equicordplugins/messageLoggerEnhanced/styles.css @@ -25,6 +25,10 @@ /* width: 100%; */ } +.msg-logger-modal-footer { + align-items: center; +} + .msg-logger-modal-content-container { /* max-height: 80vh; */ overflow: hidden; @@ -58,17 +62,9 @@ justify-content: space-around; } -.msg-logger-modal-from { - font-size: 14px; - margin-left: 12px; -} - -.msg-logger-modal-from-deleted { - color: var(--status-danger); -} - -.msg-logger-modal-from-edited { - color: var(--white-600); +.msg-logger-modal-danger-btn { + color: var(--white-500); + background-color: var(--button-danger-background); } .vc-log-toolbox-btn svg { @@ -83,6 +79,7 @@ display: flex; justify-content: space-between; align-items: center; + padding: 0; background-color: var(--background-secondary-alt); } @@ -95,4 +92,8 @@ .folder-upload-button { margin: 6px; padding: 4px 8px; +} + +.vc-updater-modal-content { + padding: 1rem 2rem; } \ No newline at end of file diff --git a/src/equicordplugins/messageLoggerEnhanced/types.ts b/src/equicordplugins/messageLoggerEnhanced/types.ts index 443689efc..a58c4c9de 100644 --- a/src/equicordplugins/messageLoggerEnhanced/types.ts +++ b/src/equicordplugins/messageLoggerEnhanced/types.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { Message, MessageAttachment, MessageJSON, User } from "@vencord/discord-types"; +import { Message, MessageAttachment, MessageJSON } from "@vencord/discord-types"; export interface LoggedAttachment extends MessageAttachment { fileExtension?: string | null; @@ -35,14 +35,6 @@ export interface LoggedMessageJSON extends Omit { ghostPinged?: boolean; timestamp: string; ourCache?: boolean; - channel_id: string; - id: string; - author: User; - flags: number; - bot: boolean; - mentions: string[]; - content: string; - webhookId: string | undefined; referenced_message: RefrencedMessage; message_reference: RefrencedMessage; } @@ -55,14 +47,6 @@ export interface LoggedMessage extends Message { timestamp: string; content: string; }[]; - channel_id: string; - id: string; - author: User; - flags: number; - bot: boolean; - mentions: string[]; - content: string; - webhookId: string | undefined; } export interface MessageDeletePayload { diff --git a/src/equicordplugins/messageLoggerEnhanced/utils/LimitedMap.ts b/src/equicordplugins/messageLoggerEnhanced/utils/LimitedMap.ts index 78c5eadb1..a0b381e75 100644 --- a/src/equicordplugins/messageLoggerEnhanced/utils/LimitedMap.ts +++ b/src/equicordplugins/messageLoggerEnhanced/utils/LimitedMap.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { settings } from "@equicordplugins/messageLoggerEnhanced/index"; +import { settings } from "../index"; export class LimitedMap { public map: Map = new Map(); @@ -24,10 +24,8 @@ export class LimitedMap { set(key: K, value: V) { if (settings.store.cacheLimit > 0 && this.map.size >= settings.store.cacheLimit) { - const firstKey = this.map.keys().next().value; - if (firstKey !== undefined) { - this.map.delete(firstKey); - } + // delete the first entry + this.map.delete(this.map.keys().next().value!); } this.map.set(key, value); } diff --git a/src/equicordplugins/messageLoggerEnhanced/utils/cleanUp.ts b/src/equicordplugins/messageLoggerEnhanced/utils/cleanUp.ts index c01c1835a..4e44c3c31 100644 --- a/src/equicordplugins/messageLoggerEnhanced/utils/cleanUp.ts +++ b/src/equicordplugins/messageLoggerEnhanced/utils/cleanUp.ts @@ -16,10 +16,10 @@ * along with this program. If not, see . */ -import { LoggedMessageJSON, RefrencedMessage } from "@equicordplugins/messageLoggerEnhanced/types"; import { User } from "@vencord/discord-types"; import { MessageStore } from "@webpack/common"; +import { LoggedMessageJSON, RefrencedMessage } from "../types"; import { getGuildIdByChannel, isGhostPinged } from "./index"; export function cleanupMessage(message: any, removeDetails: boolean = true): LoggedMessageJSON { @@ -111,6 +111,6 @@ export function cleanupUserObject(user: User) { avatar: user.avatar, id: user.id, bot: user.bot, - public_flags: typeof user.publicFlags !== "undefined" ? user.publicFlags : user.publicFlags + public_flags: typeof user.publicFlags !== "undefined" ? user.publicFlags : (user as any).public_flags }; } diff --git a/src/equicordplugins/messageLoggerEnhanced/utils/contextMenu.tsx b/src/equicordplugins/messageLoggerEnhanced/utils/contextMenu.tsx index 8e2f670a7..e953f5ec6 100644 --- a/src/equicordplugins/messageLoggerEnhanced/utils/contextMenu.tsx +++ b/src/equicordplugins/messageLoggerEnhanced/utils/contextMenu.tsx @@ -5,36 +5,44 @@ */ import { addContextMenuPatch, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; -import { openLogModal } from "@equicordplugins/messageLoggerEnhanced/components/LogsModal"; -import { deleteMessageIDB } from "@equicordplugins/messageLoggerEnhanced/db"; -import { settings } from "@equicordplugins/messageLoggerEnhanced/index"; +import { findStoreLazy } from "@webpack"; import { FluxDispatcher, Menu, MessageActions, React, Toasts, UserStore } from "@webpack/common"; +import { openLogModal } from "../components/LogsModal"; +import { deleteMessageIDB } from "../db"; +import { settings } from "../index"; import { addToXAndRemoveFromOpposite, ListType, removeFromX } from "."; +const SortedGuildStore = findStoreLazy("SortedGuildStore"); + const idFunctions = { + Folder: props => + props?.folderId && + SortedGuildStore?.getGuildFolders?.().find(f => f?.folderId === props.folderId)?.guildIds, Server: props => props?.guild?.id, User: props => props?.message?.author?.id || props?.user?.id, Channel: props => props.message?.channel_id || props.channel?.id -} as const; +} as Record string | string[] | null | undefined>; type idKeys = keyof typeof idFunctions; function renderListOption(listType: ListType, IdType: idKeys, props: any) { - const id = idFunctions[IdType](props); - if (!id) return null; + const rawId = idFunctions[IdType](props); + if (!rawId) return null; - const isBlocked = settings.store[listType].includes(id); + const ids = Array.isArray(rawId) ? rawId : [rawId]; + + const isBlocked = ids.every(id => settings.store[listType].includes(id)); const oppositeListType = listType === "blacklistedIds" ? "whitelistedIds" : "blacklistedIds"; - const isOppositeBlocked = settings.store[oppositeListType].includes(id); + const isOppositeBlocked = ids.some(id => settings.store[oppositeListType].includes(id)); const list = listType === "blacklistedIds" ? "Blacklist" : "Whitelist"; - const addToList = () => addToXAndRemoveFromOpposite(listType, id); - const removeFromList = () => removeFromX(listType, id); + const addToList = () => ids.forEach(id => addToXAndRemoveFromOpposite(listType, id)); + const removeFromList = () => ids.forEach(id => removeFromX(listType, id)); return ( if (!props) return; if (!children.some(child => child?.props?.id === "message-logger")) { - if (props.navId === "message" && (props.message?.deleted || props.message?.editHistory?.length > 0)) { - children.push( - - deleteMessageIDB(props.message.id) - .then(() => { - if (props.message.deleted) { - FluxDispatcher.dispatch({ - type: "MESSAGE_DELETE", - channelId: props.message.channel_id, - id: props.message.id, - mlDeleted: true - }); - } else { - props.message.editHistory = []; - } - }).catch(() => Toasts.show({ - type: Toasts.Type.FAILURE, - message: "Failed to remove message", - id: Toasts.genId() - })) - - } - /> - ); - } - children.push( + , {renderListOption("whitelistedIds", IdType as idKeys, props)} ))} + + { + props.navId === "message" + && (props.message?.deleted || props.message?.editHistory?.length > 0) + && ( + <> + + + deleteMessageIDB(props.message.id) + .then(() => { + if (props.message.deleted) { + FluxDispatcher.dispatch({ + type: "MESSAGE_DELETE", + channelId: props.message.channel_id, + id: props.message.id, + mlDeleted: true + }); + } else { + props.message.editHistory = []; + } + }).catch(() => Toasts.show({ + type: Toasts.Type.FAILURE, + message: "Failed to remove message", + id: Toasts.genId() + })) + } + /> + > + ) + } + { settings.store.hideMessageFromMessageLoggers && props.navId === "message" @@ -136,7 +151,6 @@ export const contextMenuPath: NavContextMenuPatchCallback = (children, props) => "validNonShortcutEmojis": [] }, { nonce: props.message.id }); }} - /> > ) diff --git a/src/equicordplugins/messageLoggerEnhanced/utils/index.ts b/src/equicordplugins/messageLoggerEnhanced/utils/index.ts index 0c55a1723..d87227eb8 100644 --- a/src/equicordplugins/messageLoggerEnhanced/utils/index.ts +++ b/src/equicordplugins/messageLoggerEnhanced/utils/index.ts @@ -17,11 +17,11 @@ */ import { Settings } from "@api/Settings"; -import { settings } from "@equicordplugins/messageLoggerEnhanced/index"; -import { LoggedMessageJSON } from "@equicordplugins/messageLoggerEnhanced/types"; import { findStoreLazy } from "@webpack"; import { ChannelStore, SelectedChannelStore, UserStore } from "@webpack/common"; +import { settings } from "../index"; +import { LoggedMessageJSON } from "../types"; import { findLastIndex, getGuildIdByChannel } from "./misc"; export * from "./cleanUp"; @@ -168,9 +168,11 @@ export function addToXAndRemoveFromOpposite(list: ListType, id: string) { export function addToX(list: ListType, id: string) { const items = settings.store[list] ? settings.store[list].split(",") : []; - items.push(id); - settings.store[list] = items.join(","); + if (!items.includes(id)) { + items.push(id); + settings.store[list] = items.join(","); + } } export function removeFromX(list: ListType, id: string) { @@ -178,6 +180,6 @@ export function removeFromX(list: ListType, id: string) { const index = items.indexOf(id); if (index !== -1) { items.splice(index, 1); + settings.store[list] = items.join(","); } - settings.store[list] = items.join(","); } diff --git a/src/equicordplugins/messageLoggerEnhanced/utils/misc.ts b/src/equicordplugins/messageLoggerEnhanced/utils/misc.ts index 0883049ee..66a264abf 100644 --- a/src/equicordplugins/messageLoggerEnhanced/utils/misc.ts +++ b/src/equicordplugins/messageLoggerEnhanced/utils/misc.ts @@ -16,12 +16,12 @@ * along with this program. If not, see . */ -import { DBMessageStatus } from "@equicordplugins/messageLoggerEnhanced/db"; -import { LoggedMessageJSON } from "@equicordplugins/messageLoggerEnhanced/types"; import { PluginNative } from "@utils/types"; import { findByCodeLazy, findLazy } from "@webpack"; import { ChannelStore, moment, UserStore } from "@webpack/common"; +import { DBMessageStatus } from "../db"; +import { LoggedMessageJSON } from "../types"; import { DEFAULT_IMAGE_CACHE_DIR } from "./constants"; import { DISCORD_EPOCH } from "./index"; import { memoize } from "./memoize"; @@ -156,6 +156,12 @@ export function getNative(): PluginNative { writeImageNative: async () => { }, chooseFile: async () => "", downloadAttachment: async () => ({ error: "web", path: null }), + startNativeLogExport: async () => "" as any, + finishNativeLogExport: async () => { }, + writeNativeLogChunk: async () => { }, + startNativeLogImport: async () => "" as any, + readNativeLogChunk: async () => null, + closeNativeLogImport: async () => { } } satisfies PluginNative; return Native; diff --git a/src/equicordplugins/messageLoggerEnhanced/utils/parseQuery.ts b/src/equicordplugins/messageLoggerEnhanced/utils/parseQuery.ts index 62a5c4afc..27d6a11ed 100644 --- a/src/equicordplugins/messageLoggerEnhanced/utils/parseQuery.ts +++ b/src/equicordplugins/messageLoggerEnhanced/utils/parseQuery.ts @@ -16,9 +16,9 @@ * along with this program. If not, see . */ -import { LoggedMessageJSON } from "@equicordplugins/messageLoggerEnhanced/types"; import { ChannelStore, GuildStore } from "@webpack/common"; +import { LoggedMessageJSON } from "../types"; import { getGuildIdByChannel } from "./index"; import { memoize } from "./memoize"; diff --git a/src/equicordplugins/messageLoggerEnhanced/utils/saveImage/ImageManager.ts b/src/equicordplugins/messageLoggerEnhanced/utils/saveImage/ImageManager.ts index 229ef361e..655665d46 100644 --- a/src/equicordplugins/messageLoggerEnhanced/utils/saveImage/ImageManager.ts +++ b/src/equicordplugins/messageLoggerEnhanced/utils/saveImage/ImageManager.ts @@ -23,11 +23,12 @@ import { keys, set, } from "@api/DataStore"; -import { Flogger, Native } from "@equicordplugins/messageLoggerEnhanced"; -import { LoggedAttachment } from "@equicordplugins/messageLoggerEnhanced/types"; -import { DEFAULT_IMAGE_CACHE_DIR } from "@equicordplugins/messageLoggerEnhanced/utils/constants"; import { sleep } from "@utils/misc"; +import { Flogger, Native } from "../.."; +import { LoggedAttachment } from "../../types"; +import { DEFAULT_IMAGE_CACHE_DIR } from "../constants"; + const ImageStore = createStore("MessageLoggerImageData", "MessageLoggerImageStore"); interface IDBSavedImage { attachmentId: string, path: string; } diff --git a/src/equicordplugins/messageLoggerEnhanced/utils/saveImage/index.ts b/src/equicordplugins/messageLoggerEnhanced/utils/saveImage/index.ts index 21d2ab36e..e0e39e2ca 100644 --- a/src/equicordplugins/messageLoggerEnhanced/utils/saveImage/index.ts +++ b/src/equicordplugins/messageLoggerEnhanced/utils/saveImage/index.ts @@ -16,11 +16,11 @@ * along with this program. If not, see . */ -import { Flogger, settings } from "@equicordplugins/messageLoggerEnhanced"; -import { LoggedAttachment, LoggedMessage, LoggedMessageJSON } from "@equicordplugins/messageLoggerEnhanced/types"; -import { memoize } from "@equicordplugins/messageLoggerEnhanced/utils/memoize"; import { MessageAttachment } from "@vencord/discord-types"; +import { Flogger, settings } from "../.."; +import { LoggedAttachment, LoggedMessage, LoggedMessageJSON } from "../../types"; +import { memoize } from "../memoize"; import { deleteImage, downloadAttachment, getImage, } from "./ImageManager"; export function getFileExtension(str: string) { diff --git a/src/equicordplugins/messageLoggerEnhanced/utils/settingsUtils.ts b/src/equicordplugins/messageLoggerEnhanced/utils/settingsUtils.ts index 7a2825846..329843e80 100644 --- a/src/equicordplugins/messageLoggerEnhanced/utils/settingsUtils.ts +++ b/src/equicordplugins/messageLoggerEnhanced/utils/settingsUtils.ts @@ -16,59 +16,51 @@ * along with this program. If not, see . */ -import { addMessagesBulkIDB, DBMessageRecord, getAllMessagesIDB } from "@equicordplugins/messageLoggerEnhanced/db"; -import { LoggedMessage, LoggedMessageJSON } from "@equicordplugins/messageLoggerEnhanced/types"; +import JSONParser from "@streamparser/json/jsonparser.js"; import { chooseFile as chooseFileWeb } from "@utils/web"; import { Toasts } from "@webpack/common"; +import { showSaveFilePicker } from "native-file-system-adapter"; import { Native } from ".."; - -async function getLogContents(): Promise { - if (IS_WEB) { - const file = await chooseFileWeb(".json"); - return new Promise((resolve, reject) => { - if (!file) return reject("No file selected"); - - const reader = new FileReader(); - reader.onload = () => resolve(reader.result as string); - reader.onerror = reject; - reader.readAsText(file); - }); - } - - const settings = await Native.getSettings(); - return Native.chooseFile("Logs", [{ extensions: ["json"], name: "logs" }], settings.logsDir); -} +import { addMessagesBulkIDB, iterateAllMessagesIDB } from "../db"; +import { LoggedMessageJSON } from "../types"; export async function importLogs() { try { - const content = await getLogContents(); - const data = JSON.parse(content) as { messages: DBMessageRecord[]; }; + let count = 0; + const batchSize = 50; + let batch: LoggedMessageJSON[] = []; - let messages: LoggedMessageJSON[] = []; + for await (const item of iterateLogItems()) { + const message = item.message || item; + if (!message || !message.id || !message.channel_id || !message.timestamp) continue; - if ((data as any).deletedMessages || (data as any).editedMessages) { - messages = Object.values((data as unknown as LoggedMessage)).filter(m => m.message).map(m => m.message) as LoggedMessageJSON[]; - } else - messages = data.messages.map(m => m.message); + batch.push(message); - if (!Array.isArray(messages)) { - throw new Error("Invalid log file format"); + if (batch.length >= batchSize) { + await addMessagesBulkIDB(batch); + count += batch.length; + batch = []; + } } - if (!messages.length) { - throw new Error("No messages found in log file"); + if (batch.length > 0) { + await addMessagesBulkIDB(batch); + count += batch.length; } - if (!messages.every(m => m.id && m.channel_id && m.timestamp)) { - throw new Error("Invalid message format"); + if (count === 0) { + Toasts.show({ + id: Toasts.genId(), + message: "No messages found in log file", + type: Toasts.Type.FAILURE + }); + return; } - await addMessagesBulkIDB(messages); - Toasts.show({ id: Toasts.genId(), - message: "Successfully imported logs", + message: `Successfully imported ${count} logs`, type: Toasts.Type.SUCCESS }); } catch (e) { @@ -86,22 +78,144 @@ export async function importLogs() { export async function exportLogs() { const filename = "message-logger-logs-idb.json"; - const messages = await getAllMessagesIDB(); - const data = JSON.stringify({ messages }, null, 2); + try { + if (!IS_WEB) { + const streamId = await Native.startNativeLogExport(filename); - if (IS_WEB || IS_VESKTOP || IS_EQUIBOP || !DiscordNative) { - const file = new File([data], filename, { type: "application/json" }); - const a = document.createElement("a"); - a.href = URL.createObjectURL(file); - a.download = filename; + await Native.writeNativeLogChunk(streamId, '{\n "messages": [\n'); - document.body.appendChild(a); - a.click(); - setImmediate(() => { - URL.revokeObjectURL(a.href); - document.body.removeChild(a); + let first = true; + for await (const record of iterateAllMessagesIDB()) { + const prefix = first ? "" : ",\n"; + first = false; + + const chunk = prefix + " " + JSON.stringify(record); + + await Native.writeNativeLogChunk(streamId, chunk); + } + + await Native.writeNativeLogChunk(streamId, "\n ]\n}"); + await Native.finishNativeLogExport(streamId); + + Toasts.show({ + id: Toasts.genId(), + message: "Successfully exported logs", + type: Toasts.Type.SUCCESS + }); + return; + } + + // if check needed so esbuild doenst include native-file-system-adapter in native builds + if (IS_WEB) { + const handle = await showSaveFilePicker({ + suggestedName: filename, + types: [{ + description: "JSON File", + accept: { "application/json": [".json"] }, + }], + }); + + const writable = await handle.createWritable(); + const writer = writable.getWriter(); + const encoder = new TextEncoder(); + + await writer.write(encoder.encode('{\n "messages": [\n')); + + let first = true; + let count = 0; + for await (const records of iterateAllMessagesIDB()) { + const prefix = first ? "" : ",\n"; + first = false; + const chunk = prefix + " " + JSON.stringify(records); + await writer.write(encoder.encode(chunk)); + count += records.length; + } + + await writer.write(encoder.encode("\n ]\n}")); + await writer.close(); + + Toasts.show({ + id: Toasts.genId(), + message: `Successfully exported ${count} logs`, + type: Toasts.Type.SUCCESS + }); + } + } catch (e) { + console.error(e); + + Toasts.show({ + id: Toasts.genId(), + message: "Error exporting logs. Check the console for more information", + type: Toasts.Type.FAILURE }); - } else { - DiscordNative.fileManager.saveWithDialog(data, filename); + } +} + +async function* parseJsonStream(readChunk: () => Promise) { + const parser = new JSONParser({ + paths: ["$.messages.*"], + keepStack: false, + }); + const queue: any[] = []; + let error: Error | null = null; + + parser.onValue = ({ value }) => { + queue.push(value); + }; + + parser.onError = (err: Error) => { + error = err; + }; + + try { + while (true) { + if (error) throw error; + while (queue.length > 0) { + yield queue.shift(); + } + + const chunk = await readChunk(); + if (chunk === null) break; + + parser.write(chunk); + } + } catch (e) { + throw e; + } finally { + if (!parser.isEnded) + parser.end(); + } + + if (error) throw error; + while (queue.length > 0) { + yield queue.shift(); + } +} + +async function* iterateLogItems(): AsyncGenerator { + if (IS_WEB) { + const file = await chooseFileWeb(".json"); + if (!file) throw new Error("No file selected"); + + const stream = file.stream(); + const reader = stream.getReader(); + const decoder = new TextDecoder(); + + yield* parseJsonStream(async () => { + const { done, value } = await reader.read(); + if (done) return null; + return decoder.decode(value, { stream: true }); + }); + } else { + const settings = await Native.getSettings(); + const fileId = await Native.startNativeLogImport(settings.logsDir); + + try { + yield* parseJsonStream(async () => { + return await Native.readNativeLogChunk(fileId); + }); + } finally { + await Native.closeNativeLogImport(fileId); + } } } diff --git a/src/equicordplugins/messagePeek/style.css b/src/equicordplugins/messagePeek/style.css index ec4c06db8..f10713e21 100644 --- a/src/equicordplugins/messagePeek/style.css +++ b/src/equicordplugins/messagePeek/style.css @@ -37,11 +37,10 @@ gap: 4px; } -.vc-message-peek-activity-row>[class*="textXs"] { - flex: 1; +.vc-message-peek-activity-row > [class*="textXs"] { min-width: 0; } -.vc-message-peek-activity-row>[class*="vc-bactivities-row"] { +.vc-message-peek-activity-row > [class*="vc-bactivities-row"] { flex-shrink: 0; -} +} \ No newline at end of file diff --git a/src/plugins/_api/memberListDecorators/index.tsx b/src/plugins/_api/memberListDecorators/index.tsx index 12e9fec4a..65e2b24c3 100644 --- a/src/plugins/_api/memberListDecorators/index.tsx +++ b/src/plugins/_api/memberListDecorators/index.tsx @@ -44,6 +44,20 @@ export default definePlugin({ match: /decorators:(\i\.isSystemDM\(\)\?.{0,100}:null)/, replace: "decorators:[Vencord.Api.MemberListDecorators.__getDecorators(arguments[0],'dm'),$1]" } + }, + // fix discords styling for now + { + find: '"AvatarWithText"', + replacement: [ + { + match: /(?<=className:.{0,10}),\{.{0,10}\}\)(?=,children:)/, + replace: ')+" vc-member-list-decorators-display-names"' + }, + { + match: /(?<=className:\i\.\i)(?=,children:\i\}\))/, + replace: '+" vc-member-list-decorators-display-names"' + } + ] } ] }); diff --git a/src/plugins/_api/memberListDecorators/style.css b/src/plugins/_api/memberListDecorators/style.css index bd4afda9d..5d840b3a4 100644 --- a/src/plugins/_api/memberListDecorators/style.css +++ b/src/plugins/_api/memberListDecorators/style.css @@ -9,3 +9,10 @@ /* Margin to match default Discord decorators */ margin-left: 0.25em; } + +.vc-member-list-decorators-display-names { + min-width: 0; + overflow: visible; + text-overflow: clip; + white-space: normal +} \ No newline at end of file diff --git a/src/plugins/showMeYourName/index.tsx b/src/plugins/showMeYourName/index.tsx index ee0af0332..7687b7fba 100644 --- a/src/plugins/showMeYourName/index.tsx +++ b/src/plugins/showMeYourName/index.tsx @@ -28,7 +28,7 @@ const symbolPattern = /^[\p{S}\p{P}]{1,3}$/iu; const templatePattern = /(?:\{(?:custom|friend|nick|display|user)(?:,\s*(?:custom|friend|nick|display|user))*\})/iu; type CustomNicknameData = Record; -export let customNicknames: CustomNicknameData = {}; +let customNicknames: CustomNicknameData = {}; function adjustHex(color: string, percent: number): string { let hex = color.replace("#", ""); @@ -897,7 +897,6 @@ export default definePlugin({ tags: ["SMYN", "Nicknames", "Custom Nicknames",], isModified: true, settings, - customNicknames, patches: [ { @@ -1059,6 +1058,7 @@ export default definePlugin({ handleHoveringMessage, addHoveringReactionPopout, removeHoveringReactionPopout, + getMessageName, getMessageNameText, getMessageNameElement, getMentionNameElement, diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 28582ddd1..51cd08d20 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -109,6 +109,7 @@ export function isEquicordGuild(id: string | null | undefined, isGuildId: boolea if (!id) return false; if (isGuildId) return id === GUILD_ID; const channel = ChannelStore.getChannel(id); + if (!channel) return false; return channel.guild_id === GUILD_ID; } @@ -121,7 +122,8 @@ export function isEquicordSupport(userId: string | null | undefined): boolean { if (!userId) return false; const member = GuildMemberStore.getMember(GUILD_ID, userId); - return member?.roles?.includes(EQUICORD_HELPERS) || false; + if (!member) return false; + return member.roles.includes(EQUICORD_HELPERS) || false; } export function removeFromArray(arr: T[], predicate: (e: T) => boolean) {