This commit is contained in:
thororen 2024-04-17 14:29:47 -04:00
parent 538b87062a
commit ea7451bcdc
326 changed files with 24876 additions and 2280 deletions

View file

@ -0,0 +1,60 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { LazyComponent } from "@utils/react";
import { filters, find } from "@webpack";
import { openLogModal } from "./LogsModal";
const HeaderBarIcon = LazyComponent(() => {
const filter = filters.byCode(".HEADER_BAR_BADGE");
return find(m => m.Icon && filter(m.Icon)).Icon;
});
export function OpenLogsIcon() {
return (
<svg
stroke="currentColor"
fill="none"
strokeWidth="0"
viewBox="0 0 15 15"
height={24}
width={24}
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
fill="currentColor"
d="M3 2.5C3 2.22386 3.22386 2 3.5 2H9.08579C9.21839 2 9.34557 2.05268 9.43934 2.14645L11.8536 4.56066C11.9473 4.65443 12 4.78161 12 4.91421V12.5C12 12.7761 11.7761 13 11.5 13H3.5C3.22386 13 3 12.7761 3 12.5V2.5ZM3.5 1C2.67157 1 2 1.67157 2 2.5V12.5C2 13.3284 2.67157 14 3.5 14H11.5C12.3284 14 13 13.3284 13 12.5V4.91421C13 4.51639 12.842 4.13486 12.5607 3.85355L10.1464 1.43934C9.86514 1.15804 9.48361 1 9.08579 1H3.5ZM4.5 4C4.22386 4 4 4.22386 4 4.5C4 4.77614 4.22386 5 4.5 5H7.5C7.77614 5 8 4.77614 8 4.5C8 4.22386 7.77614 4 7.5 4H4.5ZM4.5 7C4.22386 7 4 7.22386 4 7.5C4 7.77614 4.22386 8 4.5 8H10.5C10.7761 8 11 7.77614 11 7.5C11 7.22386 10.7761 7 10.5 7H4.5ZM4.5 10C4.22386 10 4 10.2239 4 10.5C4 10.7761 4.22386 11 4.5 11H10.5C10.7761 11 11 10.7761 11 10.5C11 10.2239 10.7761 10 10.5 10H4.5Z"
>
</path>
</svg>
);
}
export function OpenLogsButton() {
return (
<HeaderBarIcon
className="vc-log-toolbox-btn"
onClick={() => openLogModal()}
tooltip={"Open Logs"}
icon={OpenLogsIcon}
/>
);
}

View file

@ -0,0 +1,492 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { classNameFactory } from "@api/Styles";
import { openUserProfile } from "@utils/discord";
import { copyWithToast } from "@utils/misc";
import { closeAllModals, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { LazyComponent, useAwaiter } from "@utils/react";
import { find, findByCode, findByPropsLazy } from "@webpack";
import { Alerts, Button, ChannelStore, ContextMenuApi, FluxDispatcher, Menu, NavigationRouter, React, TabBar, Text, TextInput, useCallback, useMemo, useRef, useState } from "@webpack/common";
import { User } from "discord-types/general";
import { settings } from "../index";
import { clearLogs, defaultLoggedMessages, removeLog, removeLogs, savedLoggedMessages } from "../LoggedMessageManager";
import { LoggedMessage, LoggedMessageJSON, LoggedMessages } from "../types";
import { isGhostPinged, messageJsonToMessageClass, sortMessagesByDate } from "../utils";
import { doesMatch, parseQuery } from "../utils/parseQuery";
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 ChannelRecords = findByPropsLazy("PrivateChannelRecord");
const MessagePreview = LazyComponent<MessagePreviewProps>(() => find(m => m?.type?.toString().includes("previewLinkTarget:") && !m?.type?.toString().includes("HAS_THREAD")));
const ChildrenAccessories = LazyComponent<ChildrenAccProops>(() => findByCode("channelMessageProps:{message:"));
const cl = classNameFactory("msg-logger-modal-");
enum LogTabs {
DELETED = "Deleted",
EDITED = "Edited",
GHOST_PING = "Ghost Pinged"
}
interface Props {
modalProps: ModalProps;
initalQuery?: string;
}
export function LogsModal({ modalProps, initalQuery }: Props) {
const [x, setX] = useState(0);
const forceUpdate = () => setX(e => e + 1);
const [logs, _, pending] = useAwaiter(async () => savedLoggedMessages, {
fallbackValue: defaultLoggedMessages as LoggedMessages,
deps: [x]
});
const [currentTab, setCurrentTab] = useState(LogTabs.DELETED);
const [queryEh, setQuery] = useState(initalQuery ?? "");
const [sortNewest, setSortNewest] = useState(settings.store.sortNewest);
const [numDisplayedMessages, setNumDisplayedMessages] = useState(50);
const contentRef = useRef<HTMLDivElement | null>(null);
const handleLoadMore = useCallback(() => {
setNumDisplayedMessages(prevNum => prevNum + 50);
}, []);
// Flogger.log(logs, _, pending, contentRef);
// Flogger.time("hi");
const messages: string[][] = currentTab === LogTabs.DELETED || currentTab === LogTabs.GHOST_PING
? Object.values(logs?.deletedMessages ?? {})
: Object.values(logs?.editedMessages ?? {});
const flattendAndfilteredAndSortedMessages = useMemo(() => {
const { success, type, id, negate, query } = parseQuery(queryEh);
if (query === "" && !success) {
const result = messages
.flat()
.filter(m => currentTab === LogTabs.GHOST_PING ? isGhostPinged(logs[m].message!) : true)
.sort(sortMessagesByDate);
return sortNewest ? result : result.reverse();
}
const result = messages
.flat()
.filter(m =>
currentTab === LogTabs.GHOST_PING
? isGhostPinged(logs[m].message)
: true
)
.filter(m =>
logs[m]?.message != null &&
(
success === false
? true
: negate
? !doesMatch(type!, id!, logs[m].message!)
: doesMatch(type!, id!, logs[m].message!)
)
)
.filter(m =>
logs[m]?.message?.content?.toLowerCase()?.includes(query.toLowerCase()) ??
logs[m].message?.editHistory?.map(m => m.content?.toLowerCase()).includes(query.toLowerCase())
)
.sort(sortMessagesByDate);
return sortNewest ? result : result.reverse();
}, [currentTab, logs, queryEh, sortNewest]);
const visibleMessages = flattendAndfilteredAndSortedMessages.slice(0, numDisplayedMessages);
const canLoadMore = numDisplayedMessages < flattendAndfilteredAndSortedMessages.length;
// Flogger.timeEnd("hi");
return (
<ModalRoot className={cl("root")} {...modalProps} size={ModalSize.LARGE}>
<ModalHeader className={cl("header")}>
<TextInput value={queryEh} onChange={e => setQuery(e)} style={{ width: "100%" }} placeholder="Filter Messages" />
<TabBar
type="top"
look="brand"
className={cl("tab-bar")}
selectedItem={currentTab}
onItemSelect={e => {
setCurrentTab(e);
setNumDisplayedMessages(50);
contentRef.current?.firstElementChild?.scrollTo(0, 0);
// forceUpdate();
}}
>
<TabBar.Item
className={cl("tab-bar-item")}
id={LogTabs.DELETED}
>
Deleted
</TabBar.Item>
<TabBar.Item
className={cl("tab-bar-item")}
id={LogTabs.EDITED}
>
Edited
</TabBar.Item>
<TabBar.Item
className={cl("tab-bar-item")}
id={LogTabs.GHOST_PING}
>
Ghost Pinged
</TabBar.Item>
</TabBar>
</ModalHeader>
<div style={{ opacity: modalProps.transitionState === 1 ? "1" : "0" }} className={cl("content-container")} ref={contentRef}>
{
modalProps.transitionState === 1 &&
<ModalContent
className={cl("content")}
>
{
pending || logs == null || messages.length === 0
? <EmptyLogs />
: (
<LogsContentMemo
visibleMessages={visibleMessages}
canLoadMore={canLoadMore}
tab={currentTab}
logs={logs}
sortNewest={sortNewest}
forceUpdate={forceUpdate}
handleLoadMore={handleLoadMore}
/>
)
}
</ModalContent>
}
</div>
<ModalFooter>
<Button
color={Button.Colors.RED}
onClick={() => Alerts.show({
title: "Clear Logs",
body: "Are you sure you want to clear all the logs",
confirmText: "Clear",
confirmColor: Button.Colors.RED,
cancelText: "Cancel",
onConfirm: async () => {
await clearLogs();
forceUpdate();
}
})}
>
Clear All Logs
</Button>
<Button
style={{ marginRight: "16px" }}
color={Button.Colors.YELLOW}
disabled={visibleMessages.length === 0}
onClick={() => Alerts.show({
title: "Clear Logs",
body: `Are you sure you want to clear ${visibleMessages.length} logs`,
confirmText: "Clear",
confirmColor: Button.Colors.RED,
cancelText: "Cancel",
onConfirm: async () => {
await removeLogs(visibleMessages);
forceUpdate();
}
})}
>
Clear Visible Logs
</Button>
<Button
look={Button.Looks.LINK}
color={Button.Colors.PRIMARY}
onClick={() => {
setSortNewest(e => {
const val = !e;
settings.store.sortNewest = val;
return val;
});
contentRef.current?.firstElementChild?.scrollTo(0, 0);
}}
>
Sort {sortNewest ? "Oldest First" : "Newest First"}
</Button>
</ModalFooter>
</ModalRoot>
);
}
interface LogContentProps {
logs: LoggedMessages,
sortNewest: boolean;
tab: LogTabs;
visibleMessages: string[];
canLoadMore: boolean;
forceUpdate: () => void;
handleLoadMore: () => void;
}
function LogsContent({ logs, visibleMessages, canLoadMore, sortNewest, tab, forceUpdate, handleLoadMore }: LogContentProps) {
if (visibleMessages.length === 0)
return <NoResults tab={tab} />;
return (
<div className={cl("content-inner")}>
{visibleMessages
.map((id, i) => (
<LMessage
key={id}
log={logs[id] as { message: LoggedMessageJSON; }}
forceUpdate={forceUpdate}
isGroupStart={isGroupStart(logs[id]?.message, logs[visibleMessages[i - 1]]?.message, sortNewest)}
/>
))}
{
canLoadMore &&
<Button
style={{ marginTop: "1rem", width: "100%" }}
size={Button.Sizes.SMALL} onClick={() => handleLoadMore()}
>
Load More
</Button>
}
</div>
);
}
const LogsContentMemo = LazyComponent(() => React.memo(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("empty-logs", "content-inner")} style={{ textAlign: "center" }}>
<Text variant="text-lg/normal">
No results in <b>{tab}</b>.
</Text>
<Text variant="text-lg/normal" style={{ marginTop: "0.2rem" }}>
Maybe try <b>{nextTab}</b> or <b>{lastTab}</b>
</Text>
</div>
);
}
function EmptyLogs() {
return (
<div className={cl("empty-logs", "content-inner")} style={{ textAlign: "center" }}>
<Text variant="text-lg/normal">
Empty eh
</Text>
</div>
);
}
interface LMessageProps {
log: { message: LoggedMessageJSON; };
isGroupStart: boolean,
forceUpdate: () => void;
}
function LMessage({ log, isGroupStart, forceUpdate, }: LMessageProps) {
const message = useMemo(() => messageJsonToMessageClass(log), [log]);
if (!message) return null;
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={() =>
removeLog(log.message.id)
.then(() => {
forceUpdate();
})
}
/>
</Menu.Menu>
);
}}>
<MessagePreview
className={`${cl("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 ChannelRecords.PrivateChannelRecord({ id: "" }),
message,
compact: false,
groupId: "1",
id: message.id,
isLastItem: false,
isHighlight: false,
renderContentOnly: false,
}}
hasSpoilerEmbeds={false}
isInteracting={false}
showClydeAiEmbeds={true}
isAutomodBlockedMessage={false}
/>
}
/>
</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;
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;
}