453 lines
18 KiB
TypeScript
453 lines
18 KiB
TypeScript
/*
|
|
* Vencord, a Discord client mod
|
|
* Copyright (c) 2024 Vendicated and contributors
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
import { BaseText } from "@components/BaseText";
|
|
import { Button } from "@components/Button";
|
|
import { Flex } from "@components/Flex";
|
|
import { InfoIcon } from "@components/Icons";
|
|
import { Link } from "@components/Link";
|
|
import { copyWithToast, openUserProfile } from "@utils/discord";
|
|
import { closeAllModals, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
|
|
import { LazyComponent } from "@utils/react";
|
|
import { type User } from "@vencord/discord-types";
|
|
import { find, findByCode, findByCodeLazy } from "@webpack";
|
|
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 { cl, settings } from "../index";
|
|
import { LoggedMessage, LoggedMessageJSON } from "../types";
|
|
import { messageJsonToMessageClass } from "../utils";
|
|
import { importLogs } from "../utils/settingsUtils";
|
|
import { useMessages } from "./hooks";
|
|
|
|
export interface MessagePreviewProps {
|
|
className: string;
|
|
author: User;
|
|
message: LoggedMessage;
|
|
compact: boolean;
|
|
isGroupStart: boolean;
|
|
hideSimpleEmbedContent: boolean;
|
|
|
|
childrenAccessories: any;
|
|
}
|
|
|
|
export interface ChildrenAccProops {
|
|
channelMessageProps: {
|
|
compact: boolean;
|
|
channel: any;
|
|
message: LoggedMessage;
|
|
groupId: string;
|
|
id: string;
|
|
isLastItem: boolean;
|
|
isHighlight: boolean;
|
|
renderContentOnly: boolean;
|
|
};
|
|
hasSpoilerEmbeds: boolean;
|
|
isInteracting: boolean;
|
|
isAutomodBlockedMessage: boolean;
|
|
showClydeAiEmbeds: boolean;
|
|
}
|
|
|
|
const PrivateChannelRecord = findByCodeLazy(".is_message_request_timestamp,");
|
|
const MessagePreview = LazyComponent<MessagePreviewProps>(() => find(m => m?.type?.toString().includes("previewLinkTarget:") && !m?.type?.toString().includes("HAS_THREAD")));
|
|
const ChildrenAccessories = LazyComponent<ChildrenAccProops>(() => findByCode("channelMessageProps:{message:"));
|
|
|
|
export enum LogTabs {
|
|
DELETED = "Deleted",
|
|
EDITED = "Edited",
|
|
GHOST_PING = "Ghost Pinged"
|
|
}
|
|
|
|
interface Props {
|
|
modalProps: ModalProps;
|
|
initalQuery?: string;
|
|
}
|
|
|
|
export function LogsModal({ modalProps, initalQuery }: Props) {
|
|
const [currentTab, setCurrentTab] = useState(LogTabs.DELETED);
|
|
const [queryEh, setQuery] = useState(initalQuery ?? "");
|
|
const [sortNewest, setSortNewest] = useState(settings.store.sortNewest);
|
|
const [numDisplayedMessages, setNumDisplayedMessages] = useState(settings.store.messagesToDisplayAtOnceInLogs);
|
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
const { messages, total, statusTotal, pending, reset } = useMessages(queryEh, currentTab, sortNewest, numDisplayedMessages);
|
|
|
|
return (
|
|
<ModalRoot className={cl("modal-root")} {...modalProps} size={ModalSize.LARGE}>
|
|
<ModalHeader className={cl("modal-header")}>
|
|
<TextInput value={queryEh} onChange={e => setQuery(e)} style={{ width: "100%" }} placeholder="Filter Messages" />
|
|
<TabBar
|
|
type="top"
|
|
look="brand"
|
|
className={cl("modal-tab-bar")}
|
|
selectedItem={currentTab}
|
|
onItemSelect={e => {
|
|
setCurrentTab(e);
|
|
setNumDisplayedMessages(settings.store.messagesToDisplayAtOnceInLogs);
|
|
contentRef.current?.firstElementChild?.scrollTo(0, 0);
|
|
}}
|
|
>
|
|
<TabBar.Item
|
|
className={cl("modal-tab-bar-item")}
|
|
id={LogTabs.DELETED}
|
|
>
|
|
Deleted
|
|
</TabBar.Item>
|
|
<TabBar.Item
|
|
className={cl("modal-tab-bar-item")}
|
|
id={LogTabs.EDITED}
|
|
>
|
|
Edited
|
|
</TabBar.Item>
|
|
<TabBar.Item
|
|
className={cl("modal-tab-bar-item")}
|
|
id={LogTabs.GHOST_PING}
|
|
>
|
|
Ghost Pinged
|
|
</TabBar.Item>
|
|
</TabBar>
|
|
</ModalHeader>
|
|
<div style={{ opacity: modalProps.transitionState === 1 ? "1" : "0" }} className={cl("modal-content-container")} ref={contentRef}>
|
|
{
|
|
modalProps.transitionState === 1 &&
|
|
<ModalContent
|
|
className={cl("modal-content")}
|
|
>
|
|
{messages != null && total === 0 && (
|
|
<EmptyLogs
|
|
hasQuery={queryEh.length !== 0}
|
|
reset={reset}
|
|
/>
|
|
)}
|
|
|
|
{!pending && messages != null && (
|
|
<LogsContentMemo
|
|
visibleMessages={messages}
|
|
canLoadMore={messages.length < statusTotal && messages.length >= settings.store.messagesToDisplayAtOnceInLogs}
|
|
tab={currentTab}
|
|
sortNewest={sortNewest}
|
|
reset={reset}
|
|
handleLoadMore={() => setNumDisplayedMessages(e => e + settings.store.messagesToDisplayAtOnceInLogs)}
|
|
/>
|
|
)}
|
|
</ModalContent>
|
|
}
|
|
</div>
|
|
<ModalFooter className={cl("footer")}>
|
|
<Button
|
|
variant="dangerPrimary"
|
|
onClick={() => Alerts.show({
|
|
title: "Clear Logs",
|
|
body: "Are you sure you want to clear all the logs",
|
|
confirmText: "Clear",
|
|
// @ts-expect-error not typed
|
|
confirmVariant: "critical-primary",
|
|
cancelText: "Cancel",
|
|
onConfirm: async () => {
|
|
await clearMessagesIDB();
|
|
reset();
|
|
}
|
|
|
|
})}
|
|
>
|
|
Clear All Logs
|
|
</Button>
|
|
<Button
|
|
style={{ marginRight: "16px" }}
|
|
variant="dangerSecondary"
|
|
disabled={messages?.length === 0}
|
|
onClick={() => Alerts.show({
|
|
title: "Clear Logs",
|
|
body: `Are you sure you want to clear ${messages.length} logs`,
|
|
confirmText: "Clear",
|
|
// @ts-expect-error not typed
|
|
confirmVariant: "critical-primary",
|
|
cancelText: "Cancel",
|
|
onConfirm: async () => {
|
|
await deleteMessagesBulkIDB(messages.map(e => e.message_id));
|
|
reset();
|
|
}
|
|
})}
|
|
>
|
|
Clear Visible Logs
|
|
</Button>
|
|
<Link
|
|
style={{ marginRight: "1rem" }}
|
|
onClick={() => {
|
|
setSortNewest(e => {
|
|
const val = !e;
|
|
settings.store.sortNewest = val;
|
|
return val;
|
|
});
|
|
contentRef.current?.firstElementChild?.scrollTo(0, 0);
|
|
}}
|
|
>
|
|
Sort {sortNewest ? "Oldest First" : "Newest First"}
|
|
</Link>
|
|
</ModalFooter>
|
|
</ModalRoot>
|
|
);
|
|
}
|
|
|
|
interface LogContentProps {
|
|
sortNewest: boolean;
|
|
tab: LogTabs;
|
|
visibleMessages: DBMessageRecord[];
|
|
canLoadMore: boolean;
|
|
reset: () => void;
|
|
handleLoadMore: () => void;
|
|
}
|
|
|
|
function LogsContent({ visibleMessages, canLoadMore, sortNewest, tab, reset, handleLoadMore }: LogContentProps) {
|
|
if (visibleMessages.length === 0)
|
|
return <NoResults tab={tab} />;
|
|
|
|
return (
|
|
<div className={cl("modal-content-inner")}>
|
|
{visibleMessages
|
|
.map(({ message }, i) => (
|
|
<LMessage
|
|
key={message.id}
|
|
log={{ message }}
|
|
reset={reset}
|
|
isGroupStart={isGroupStart(message, visibleMessages[i - 1]?.message, sortNewest)}
|
|
/>
|
|
))}
|
|
{
|
|
canLoadMore &&
|
|
<Button
|
|
style={{ marginTop: "1rem", width: "100%" }}
|
|
size="small" onClick={() => handleLoadMore()}
|
|
>
|
|
Load More
|
|
</Button>
|
|
}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const LogsContentMemo = LazyComponent(() => LogsContent);
|
|
|
|
function NoResults({ tab }: { tab: LogTabs; }) {
|
|
const generateSuggestedTabs = (tab: LogTabs) => {
|
|
switch (tab) {
|
|
case LogTabs.DELETED:
|
|
return { nextTab: LogTabs.EDITED, lastTab: LogTabs.GHOST_PING };
|
|
case LogTabs.EDITED:
|
|
return { nextTab: LogTabs.GHOST_PING, lastTab: LogTabs.DELETED };
|
|
case LogTabs.GHOST_PING:
|
|
return { nextTab: LogTabs.DELETED, lastTab: LogTabs.EDITED };
|
|
default:
|
|
return { nextTab: "", lastTab: "" };
|
|
}
|
|
};
|
|
|
|
const { nextTab, lastTab } = generateSuggestedTabs(tab);
|
|
|
|
return (
|
|
<div className={cl("modal-empty-logs", "modal-content-inner")} style={{ textAlign: "center" }}>
|
|
<BaseText size="lg">
|
|
No results in <b>{tab}</b>.
|
|
</BaseText>
|
|
<BaseText size="lg" style={{ marginTop: "0.2rem" }}>
|
|
Maybe try <b>{nextTab}</b> or <b>{lastTab}</b>
|
|
</BaseText>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function EmptyLogs({ hasQuery, reset: forceUpdate }: { hasQuery: boolean; reset: () => void; }) {
|
|
return (
|
|
<div className={cl("modal-empty-logs", "modal-content-inner")} style={{ textAlign: "center" }}>
|
|
<Flex flexDirection="column" style={{ position: "relative" }}>
|
|
|
|
<BaseText size="lg">
|
|
Empty eh
|
|
</BaseText>
|
|
|
|
{!hasQuery && (
|
|
<>
|
|
<Tooltip text="ML Enhanced now stores logs in indexeddb. You need to import your old logs from the logs directory. Importing wont overwrite existing logs">
|
|
{({ onMouseEnter, onMouseLeave }) => (
|
|
<div
|
|
className={cl("modal-info-icon")}
|
|
onMouseEnter={onMouseEnter}
|
|
onMouseLeave={onMouseLeave}
|
|
>
|
|
<InfoIcon />
|
|
</div>
|
|
)}
|
|
</Tooltip>
|
|
|
|
<Button onClick={() => importLogs().then(() => forceUpdate())}>
|
|
Import Logs
|
|
</Button>
|
|
</>
|
|
)}
|
|
</Flex>
|
|
</div>
|
|
);
|
|
|
|
}
|
|
|
|
interface LMessageProps {
|
|
log: { message: LoggedMessageJSON; };
|
|
isGroupStart: boolean,
|
|
reset: () => void;
|
|
}
|
|
function LMessage({ log, isGroupStart, reset, }: LMessageProps) {
|
|
const message = useMemo(() => messageJsonToMessageClass(log), [log]);
|
|
|
|
if (!message) return null;
|
|
|
|
const channel = ChannelStore.getChannel(message?.channel_id);
|
|
const guild = GuildStore.getGuild(channel?.guild_id);
|
|
|
|
return (
|
|
<div
|
|
onContextMenu={e => {
|
|
ContextMenuApi.openContextMenu(e, () =>
|
|
<Menu.Menu
|
|
navId="message-logger"
|
|
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
|
|
aria-label="Message Logger"
|
|
>
|
|
|
|
<Menu.MenuItem
|
|
key="jump-to-message"
|
|
id="jump-to-message"
|
|
label="Jump To Message"
|
|
action={() => {
|
|
NavigationRouter.transitionTo(`/channels/${ChannelStore.getChannel(message.channel_id)?.guild_id ?? "@me"}/${message.channel_id}${message.id ? "/" + message.id : ""}`);
|
|
closeAllModals();
|
|
}}
|
|
/>
|
|
<Menu.MenuItem
|
|
key="open-user-profile"
|
|
id="open-user-profile"
|
|
label="Open user profile"
|
|
action={() => {
|
|
closeAllModals();
|
|
openUserProfile(message.author.id);
|
|
}}
|
|
/>
|
|
|
|
<Menu.MenuItem
|
|
key="copy-content"
|
|
id="copy-content"
|
|
label="Copy Content"
|
|
action={() => copyWithToast(message.content)}
|
|
/>
|
|
|
|
<Menu.MenuItem
|
|
key="copy-user-id"
|
|
id="copy-user-id"
|
|
label="Copy User ID"
|
|
action={() => copyWithToast(message.author.id)}
|
|
/>
|
|
|
|
<Menu.MenuItem
|
|
key="copy-message-id"
|
|
id="copy-message-id"
|
|
label="Copy Message ID"
|
|
action={() => copyWithToast(message.id)}
|
|
/>
|
|
|
|
<Menu.MenuItem
|
|
key="copy-channel-id"
|
|
id="copy-channel-id"
|
|
label="Copy Channel ID"
|
|
action={() => copyWithToast(message.channel_id)}
|
|
/>
|
|
|
|
{
|
|
log.message.guildId != null
|
|
&& (
|
|
<Menu.MenuItem
|
|
key="copy-server-id"
|
|
id="copy-server-id"
|
|
label="Copy Server ID"
|
|
action={() => copyWithToast(log.message.guildId!)}
|
|
/>
|
|
)
|
|
}
|
|
|
|
<Menu.MenuItem
|
|
key="delete-log"
|
|
id="delete-log"
|
|
label="Delete Log"
|
|
color="danger"
|
|
action={() =>
|
|
deleteMessageIDB(log.message.id).then(() => reset())
|
|
}
|
|
/>
|
|
|
|
</Menu.Menu>
|
|
);
|
|
}}>
|
|
<MessagePreview
|
|
className={`${cl("modal-msg-preview")} ${message.deleted ? "messagelogger-deleted" : ""}`}
|
|
author={message.author}
|
|
message={message}
|
|
compact={false}
|
|
isGroupStart={isGroupStart}
|
|
hideSimpleEmbedContent={false}
|
|
|
|
childrenAccessories={
|
|
<ChildrenAccessories
|
|
channelMessageProps={{
|
|
channel: ChannelStore.getChannel(message.channel_id) || new PrivateChannelRecord({ id: "" }),
|
|
message,
|
|
compact: false,
|
|
groupId: "1",
|
|
id: message.id,
|
|
isLastItem: false,
|
|
isHighlight: false,
|
|
renderContentOnly: false,
|
|
}}
|
|
hasSpoilerEmbeds={false}
|
|
isInteracting={false}
|
|
showClydeAiEmbeds={true}
|
|
isAutomodBlockedMessage={false}
|
|
/>
|
|
}
|
|
/>
|
|
{settings.store.ShowWhereMessageIsFrom && channel?.isDM() && message?.author && (
|
|
<span className={`${cl("modal-from")} ${message.deleted ? cl("modal-from-deleted") : cl("modal-from-edited")}`}>From {message.author.username}'s DMs</span>
|
|
)}
|
|
{settings.store.ShowWhereMessageIsFrom && channel?.isGroupDM() && channel?.name && (
|
|
<span className={`${cl("modal-from")} ${message.deleted ? cl("modal-from-deleted") : cl("modal-from-edited")}`}>From {channel.name} Group DM</span>
|
|
)}
|
|
{settings.store.ShowWhereMessageIsFrom && !channel?.isDM() && !channel?.isGroupDM() && channel?.name && guild?.name && (
|
|
<span className={`${cl("modal-from")} ${message.deleted ? cl("modal-from-deleted") : cl("modal-from-edited")}`}>From {channel.name} in {guild.name}</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export const openLogModal = (initalQuery?: string) => openModal(modalProps => <LogsModal modalProps={modalProps} initalQuery={initalQuery} />);
|
|
|
|
function isGroupStart(
|
|
currentMessage: LoggedMessageJSON | undefined,
|
|
previousMessage: LoggedMessageJSON | undefined,
|
|
sortNewest: boolean
|
|
) {
|
|
if (!currentMessage || !previousMessage) return true;
|
|
|
|
if (currentMessage.id === previousMessage.id) return true;
|
|
|
|
const [newestMessage, oldestMessage] = sortNewest
|
|
? [previousMessage, currentMessage]
|
|
: [currentMessage, previousMessage];
|
|
|
|
if (newestMessage.author.id !== oldestMessage.author.id) return true;
|
|
|
|
const timeDifferenceInMinutes = Math.abs(
|
|
(new Date(newestMessage.timestamp)?.getTime() - new Date(oldestMessage.timestamp)?.getTime()) / (1000 * 60)
|
|
);
|
|
|
|
return timeDifferenceInMinutes >= 5;
|
|
}
|