HolyNotes: Removed

This commit is contained in:
thororen1234 2026-04-24 12:35:12 -04:00
parent 0397b4a2df
commit 6900c60b5b
No known key found for this signature in database
11 changed files with 0 additions and 1120 deletions

View file

@ -1,149 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Channel, Message } from "@vencord/discord-types";
import { proxyLazyWebpack } from "@webpack";
import { ChannelStore, Toasts, UserStore } from "@webpack/common";
import { AllNotebooks, Note, Notebook } from "./types";
import { deleteCacheFromDataStore, DeleteEntireStore, saveCacheToDataStore } from "./utils";
export const noteHandlerCache = new Map<string, Notebook>();
function showToast(message: string, type: typeof Toasts.Type[keyof typeof Toasts.Type] = Toasts.Type.SUCCESS) {
Toasts.show({ id: Toasts.genId(), message, type });
}
function formatNote(channel: Channel, message: Message): Note {
return {
id: message.id,
channel_id: message.channel_id,
guild_id: channel.guild_id,
content: message.content,
author: {
id: message.author.id,
avatar: message.author.avatar,
discriminator: message.author.discriminator,
username: message.author.username,
},
flags: message.flags,
timestamp: message.timestamp.toString(),
attachments: message.attachments,
embeds: message.embeds,
reactions: message.reactions,
stickerItems: message.stickerItems,
};
}
export const noteHandler = proxyLazyWebpack(() => ({
getNotes(notebook: string): Notebook | undefined {
return noteHandlerCache.get(notebook);
},
getAllNotes(): AllNotebooks {
return Object.fromEntries(noteHandlerCache.entries());
},
addNote(message: Message, notebook: string) {
const notes = this.getNotes(notebook) ?? {};
const channel = ChannelStore.getChannel(message.channel_id);
const newNotes = { [message.id]: formatNote(channel, message), ...notes };
noteHandlerCache.set(notebook, newNotes);
saveCacheToDataStore(notebook, newNotes);
showToast(`Added note to ${notebook}`);
},
deleteNote(noteId: string, notebook: string) {
const notes = this.getNotes(notebook);
if (!notes) return;
const { [noteId]: _, ...rest } = notes;
noteHandlerCache.set(notebook, rest);
saveCacheToDataStore(notebook, rest);
showToast(`Deleted note from ${notebook}`);
},
moveNote(note: Note, from: string, to: string) {
const fromNotes = this.getNotes(from);
const toNotes = this.getNotes(to);
if (!fromNotes || !toNotes) return;
const { [note.id]: _, ...restFrom } = fromNotes;
const newTo = { [note.id]: note, ...toNotes };
noteHandlerCache.set(from, restFrom);
noteHandlerCache.set(to, newTo);
saveCacheToDataStore(from, restFrom);
saveCacheToDataStore(to, newTo);
showToast(`Moved note from ${from} to ${to}`);
},
newNoteBook(name: string, silent?: boolean) {
if (noteHandlerCache.has(name)) {
showToast(`Notebook ${name} already exists`, Toasts.Type.FAILURE);
return;
}
noteHandlerCache.set(name, {});
saveCacheToDataStore(name, {});
if (!silent) showToast(`Created notebook ${name}`);
},
deleteNotebook(name: string) {
noteHandlerCache.delete(name);
deleteCacheFromDataStore(name);
showToast(`Deleted notebook ${name}`);
},
refreshAvatars() {
const notebooks = this.getAllNotes();
for (const notebookName in notebooks) {
const notebook = notebooks[notebookName];
for (const noteId in notebook) {
const note = notebook[noteId];
const user = UserStore.getUser(note.author.id);
if (user) {
note.author.avatar = user.avatar;
note.author.discriminator = user.discriminator;
note.author.username = user.username;
}
}
noteHandlerCache.set(notebookName, notebook);
saveCacheToDataStore(notebookName, notebook);
}
showToast("Refreshed avatars");
},
async deleteEverything() {
noteHandlerCache.clear();
await DeleteEntireStore();
showToast("Deleted all notes");
},
exportNotes(): AllNotebooks {
return this.getAllNotes();
},
importNotes(jsonString: string) {
let parsed: AllNotebooks;
try {
parsed = JSON.parse(jsonString);
} catch {
showToast("Invalid JSON", Toasts.Type.FAILURE);
return;
}
for (const notebook in parsed) {
noteHandlerCache.set(notebook, parsed[notebook]);
saveCacheToDataStore(notebook, parsed[notebook]);
}
showToast("Imported notes");
},
}));

View file

@ -1,38 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { resultsClasses } from "@equicordplugins/holyNotes";
export default function EmptyNotebook({ error }: { error?: Error; } = {}) {
if (error) {
console.error("[HolyNotes] Error:", error);
return (
<div className={resultsClasses.emptyResultsWrap}>
<div className={resultsClasses.emptyResultsContent} style={{ paddingBottom: 0 }}>
<div className={resultsClasses.errorImage} />
<div className={resultsClasses.emptyResultsText}>
There was an error parsing your notes. Check the console for details.
</div>
</div>
</div>
);
}
const isEasterEgg = Math.random() < 0.1;
return (
<div className={resultsClasses.emptyResultsWrap}>
<div className={resultsClasses.emptyResultsContent} style={{ paddingBottom: 0 }}>
<div className={`${resultsClasses.noResultsImage}${isEasterEgg ? ` ${resultsClasses.alt}` : ""}`} />
<div className={resultsClasses.emptyResultsText}>
{isEasterEgg
? "No notes were found. Empathy banana is here for you."
: "No notes were found in this notebook."}
</div>
</div>
</div>
);
}

View file

@ -1,75 +0,0 @@
/*
* 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 { cl } from "@equicordplugins/holyNotes";
import { noteHandler } from "@equicordplugins/holyNotes/NoteHandler";
import { downloadNotes, uploadNotes } from "@equicordplugins/holyNotes/utils";
import { CloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
function HelpSection({ title, children }: { title: string; children: React.ReactNode; }) {
return (
<>
<BaseText size="md" weight="semibold" color="text-strong">{title}</BaseText>
<BaseText size="sm" color="text-default" style={{ marginTop: 4 }}>{children}</BaseText>
<hr />
</>
);
}
export default function HelpModal({ onClose, transitionState }: ModalProps) {
return (
<ModalRoot transitionState={transitionState} size={ModalSize.MEDIUM}>
<ModalHeader separator={false} className={cl("header")}>
<div className={cl("header-content")}>
<BaseText tag="h2" size="lg" weight="semibold" className={cl("title")}>
Help
</BaseText>
<BaseText size="sm" className={cl("description")}>
Learn how to use Holy Notes
</BaseText>
</div>
<div className={cl("header-trailing")}>
<CloseButton onClick={onClose} />
</div>
</ModalHeader>
<ModalContent className={cl("content")}>
<div className="vc-help-markdown">
<HelpSection title="Adding Notes">
Right-click a message, hover over "Note Message", and select the notebook.
</HelpSection>
<HelpSection title="Deleting Notes">
Right-click a note and select "Delete Note", or hold DELETE and click a note.
</HelpSection>
<HelpSection title="Moving Notes">
Right-click a note, hover over "Move Note", and select the destination notebook.
</HelpSection>
<HelpSection title="Jump To Message">
Right-click a note and select "Jump to Message" to go to the original message.
</HelpSection>
</div>
</ModalContent>
<ModalFooter>
<Flex style={{ gap: 8, flexWrap: "wrap" }}>
<Button size="small" variant="primary" onClick={() => noteHandler.refreshAvatars()}>
Refresh Avatars
</Button>
<Button size="small" variant="secondary" onClick={uploadNotes}>
Import Notes
</Button>
<Button size="small" variant="secondary" onClick={downloadNotes}>
Export Notes
</Button>
<Button size="small" variant="dangerSecondary" onClick={() => noteHandler.deleteEverything()}>
Delete All Notes
</Button>
</Flex>
</ModalFooter>
</ModalRoot>
);
}

View file

@ -1,177 +0,0 @@
/*
* 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 ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { BookmarkIcon } from "@components/Icons";
import { CircleQuestionIcon, cl } from "@equicordplugins/holyNotes";
import { noteHandler } from "@equicordplugins/holyNotes/NoteHandler";
import { Note, Notebook } from "@equicordplugins/holyNotes/types";
import { CloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Clickable, React, Select, TextInput, Tooltip, useState } from "@webpack/common";
import Errors from "./Error";
import HelpModal from "./HelpModal";
import NotebookCreateModal from "./NotebookCreateModal";
import NotebookDeleteModal from "./NotebookDeleteModal";
import { RenderMessage } from "./RenderMessage";
const enum SortOption {
NewestAdded,
OldestAdded,
NewestMessage,
OldestMessage
}
function NotebookTabs({ notebooks, selected, onSelect }: {
notebooks: string[];
selected: string;
onSelect: (tab: string) => void;
}) {
const sorted = [...notebooks].sort((a, b) =>
a === "Main" ? -1 : b === "Main" ? 1 : a.localeCompare(b)
);
return (
<div className={cl("tabs")}>
{sorted.map(name => (
<Clickable
key={name}
className={cl("tab", selected === name && "tab-selected")}
onClick={() => onSelect(name)}
>
{name}
</Clickable>
))}
</div>
);
}
function sortNotes(notes: Note[], sort: SortOption): Note[] {
const sorted = [...notes];
switch (sort) {
case SortOption.NewestAdded:
return sorted;
case SortOption.OldestAdded:
return sorted.reverse();
case SortOption.NewestMessage:
return sorted.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
case SortOption.OldestMessage:
return sorted.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
}
}
function filterNotes(notes: Notebook, search: string): Note[] {
const arr = Object.values(notes);
if (!search) return arr;
const q = search.toLowerCase();
return arr.filter(n => n.content?.toLowerCase().includes(q));
}
export function NoteModal({ onClose, transitionState }: ModalProps) {
const [sort, setSort] = useState(SortOption.NewestAdded);
const [search, setSearch] = useState("");
const [notebook, setNotebook] = useState("Main");
const [, forceUpdate] = React.useReducer(x => x + 1, 0);
const notes = noteHandler.getNotes(notebook);
if (!notes) return null;
const filtered = filterNotes(notes, search);
const sorted = sortNotes(filtered, sort);
const total = Object.keys(notes).length;
const isMain = notebook === "Main";
return (
<ModalRoot transitionState={transitionState} size={ModalSize.LARGE}>
<ModalHeader separator={false} className={cl("header")}>
<div className={cl("header-content")}>
<Flex alignItems="center" style={{ gap: "12px" }}>
<BookmarkIcon width="24" height="24" />
<BaseText tag="h2" size="lg" weight="semibold" className={cl("title")}>
Notebook
</BaseText>
</Flex>
<BaseText size="sm" className={cl("description")}>
{search ? `${filtered.length} of ${total}` : total} {total === 1 ? "note" : "notes"}
</BaseText>
</div>
<div className={cl("header-trailing")}>
<CloseButton onClick={onClose} />
</div>
</ModalHeader>
<div className={cl("controls")}>
<TextInput
placeholder="Search notes..."
value={search}
onChange={setSearch}
/>
<Select
options={[
{ label: "Newest First", value: SortOption.NewestAdded, default: true },
{ label: "Oldest First", value: SortOption.OldestAdded },
{ label: "Newest Msg", value: SortOption.NewestMessage },
{ label: "Oldest Msg", value: SortOption.OldestMessage },
]}
serialize={String}
select={setSort}
isSelected={v => v === sort}
closeOnSelect
/>
</div>
<NotebookTabs
notebooks={Object.keys(noteHandler.getAllNotes())}
selected={notebook}
onSelect={setNotebook}
/>
<ModalContent className={cl("content")}>
<ErrorBoundary>
{sorted.length ? sorted.map(note => (
<RenderMessage
key={note.id}
note={note}
notebook={notebook}
updateParent={forceUpdate}
fromDeleteModal={false}
closeModal={onClose}
/>
)) : <Errors />}
</ErrorBoundary>
</ModalContent>
<ModalFooter>
<Flex style={{ justifyContent: "space-between", alignItems: "center", width: "100%" }}>
<Tooltip text="Help">
{({ onMouseEnter, onMouseLeave }) => (
<Clickable
className={cl("icon-button")}
onClick={() => openModal(props => <HelpModal {...props} />)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<CircleQuestionIcon size="sm" />
</Clickable>
)}
</Tooltip>
<Button
variant={isMain ? "primary" : "dangerPrimary"}
onClick={() => isMain
? openModal(props => <NotebookCreateModal {...props} />)
: openModal(props => <NotebookDeleteModal {...props} notebook={notebook} onChangeTab={setNotebook} />)
}
>
{isMain ? "Create Notebook" : "Delete Notebook"}
</Button>
</Flex>
</ModalFooter>
</ModalRoot>
);
}

View file

@ -1,55 +0,0 @@
/*
* 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 { cl } from "@equicordplugins/holyNotes";
import { noteHandler } from "@equicordplugins/holyNotes/NoteHandler";
import { CloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
import { TextInput, useState } from "@webpack/common";
export default function NotebookCreateModal({ onClose, transitionState }: ModalProps) {
const [name, setName] = useState("");
const handleCreate = () => {
if (!name.trim()) return;
noteHandler.newNoteBook(name.trim());
onClose();
};
return (
<ModalRoot transitionState={transitionState} size={ModalSize.SMALL} className={cl("create-modal")}>
<ModalHeader separator={false} className={cl("header")}>
<div className={cl("header-content")}>
<BaseText tag="h2" size="lg" weight="semibold" className={cl("title")}>
Create Notebook
</BaseText>
<BaseText size="sm" className={cl("description")}>
Enter a name for your new notebook
</BaseText>
</div>
<div className={cl("header-trailing")}>
<CloseButton onClick={onClose} />
</div>
</ModalHeader>
<ModalContent className={cl("content")}>
<TextInput
value={name}
placeholder="Notebook Name"
onChange={setName}
/>
</ModalContent>
<ModalFooter>
<Button variant="primary" onClick={handleCreate}>
Create
</Button>
<Button variant="secondary" onClick={onClose}>
Cancel
</Button>
</ModalFooter>
</ModalRoot>
);
}

View file

@ -1,68 +0,0 @@
/*
* 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 ErrorBoundary from "@components/ErrorBoundary";
import { cl } from "@equicordplugins/holyNotes";
import { noteHandler } from "@equicordplugins/holyNotes/NoteHandler";
import { CloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal";
import { React } from "@webpack/common";
import EmptyNotebook from "./Error";
import { RenderMessage } from "./RenderMessage";
interface Props extends ModalProps {
notebook: string;
onChangeTab: React.Dispatch<React.SetStateAction<string>>;
}
export default function NotebookDeleteModal({ onClose, transitionState, notebook, onChangeTab }: Props) {
const notes = noteHandler.getNotes(notebook);
const handleDelete = () => {
onClose();
onChangeTab("Main");
noteHandler.deleteNotebook(notebook);
};
const noteList = notes ? Object.values(notes) : [];
return (
<ModalRoot transitionState={transitionState} size={ModalSize.LARGE}>
<ModalHeader separator={false} className={cl("header")}>
<div className={cl("header-content")}>
<BaseText tag="h2" size="lg" weight="semibold" className={cl("title")}>
Delete "{notebook}"?
</BaseText>
<BaseText size="sm" className={cl("description")}>
{noteList.length} {noteList.length === 1 ? "note" : "notes"} will be deleted
</BaseText>
</div>
<div className={cl("header-trailing")}>
<CloseButton onClick={onClose} />
</div>
</ModalHeader>
<ModalContent className={cl("content")}>
<ErrorBoundary>
{noteList.length ? noteList.map(note => (
<RenderMessage
key={note.id}
note={note}
notebook={notebook}
fromDeleteModal
/>
)) : <EmptyNotebook />}
</ErrorBoundary>
</ModalContent>
<ModalFooter>
<Button variant="dangerPrimary" onClick={handleDelete}>
Delete Notebook
</Button>
</ModalFooter>
</ModalRoot>
);
}

View file

@ -1,180 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { CopyIcon, DeleteIcon, IDIcon, LinkIcon, OpenExternalIcon } from "@components/Icons";
import { ChannelMessage, ChannelRecord, messageClasses, MessageRecord } from "@equicordplugins/holyNotes";
import { noteHandler } from "@equicordplugins/holyNotes/NoteHandler";
import { Note } from "@equicordplugins/holyNotes/types";
import { copyToClipboard } from "@utils/clipboard";
import { fetchUserProfile } from "@utils/discord";
import { proxyLazy } from "@utils/lazy";
import { classes } from "@utils/misc";
import { ContextMenuApi, FluxDispatcher, Menu, NavigationRouter, useEffect, useReducer, useRef, UserStore } from "@webpack/common";
const UserRecord = proxyLazy(() => UserStore.getCurrentUser().constructor) as any;
interface Props {
note: Note;
notebook: string;
updateParent?: () => void;
fromDeleteModal: boolean;
closeModal?: () => void;
}
export function RenderMessage({ note, notebook, updateParent, fromDeleteModal, closeModal }: Props) {
const isHoldingDeleteRef = useRef(false);
const [, forceUpdate] = useReducer(x => x + 1, 0);
useEffect(() => {
if (!UserStore.getUser(note.author.id)) {
fetchUserProfile(note.author.id);
}
}, [note.author.id]);
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
if (e.key !== "Delete") return;
const isDown = e.type === "keydown";
if (isHoldingDeleteRef.current !== isDown) {
isHoldingDeleteRef.current = isDown;
forceUpdate();
}
};
document.addEventListener("keydown", handleKey);
document.addEventListener("keyup", handleKey);
return () => {
document.removeEventListener("keydown", handleKey);
document.removeEventListener("keyup", handleKey);
};
}, []);
const author = UserStore.getUser(note.author.id) ?? note.author;
const handleClick = () => {
if (isHoldingDeleteRef.current && !fromDeleteModal) {
noteHandler.deleteNote(note.id, notebook);
updateParent?.();
}
};
const handleContextMenu = (e: React.MouseEvent) => {
if (fromDeleteModal) return;
ContextMenuApi.openContextMenu(e, () => (
<NoteContextMenu
note={note}
notebook={notebook}
updateParent={updateParent}
closeModal={closeModal}
/>
));
};
const message = new MessageRecord({
...note,
author: new UserRecord({ ...author, bot: true }),
timestamp: new Date(note.timestamp),
embeds: note.embeds?.map(embed =>
embed.timestamp ? { ...embed, timestamp: new Date(embed.timestamp) } : embed
),
});
return (
<div
className="vc-holy-note"
style={{ margin: "8px 0", padding: "4px 0" }}
onClick={handleClick}
onContextMenu={handleContextMenu}
>
<ChannelMessage
className={classes("vc-holy-render", messageClasses?.message, messageClasses?.groupStart, messageClasses?.cozyMessage)}
key={note.id}
groupId={note.id}
id={note.id}
compact={false}
isHighlight={false}
isLastItem={false}
renderContentOnly={false}
channel={new ChannelRecord({ id: "holy-notes" })}
message={message}
/>
</div>
);
}
interface ContextMenuProps {
note: Note;
notebook: string;
updateParent?: () => void;
closeModal?: () => void;
}
function NoteContextMenu({ note, notebook, updateParent, closeModal }: ContextMenuProps) {
const allNotebooks = Object.keys(noteHandler.getAllNotes());
return (
<Menu.Menu
navId="holynotes"
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
aria-label="Holy Notes"
>
<Menu.MenuItem
label="Jump To Message"
id="jump"
icon={OpenExternalIcon}
action={() => {
NavigationRouter.transitionTo(`/channels/${note.guild_id || "@me"}/${note.channel_id}/${note.id}`);
closeModal?.();
}}
/>
<Menu.MenuItem
label="Copy Text"
id="copy-text"
icon={CopyIcon}
action={() => copyToClipboard(note.content)}
/>
{note.attachments.length > 0 && (
<Menu.MenuItem
label="Copy Attachment URL"
id="copy-url"
icon={LinkIcon}
action={() => copyToClipboard(note.attachments[0].url)}
/>
)}
<Menu.MenuItem
label="Copy ID"
id="copy-id"
icon={IDIcon}
action={() => copyToClipboard(note.id)}
/>
{allNotebooks.length > 1 && (
<Menu.MenuItem label="Move Note" id="move-note">
{allNotebooks.filter(k => k !== notebook).map(k => (
<Menu.MenuItem
key={k}
label={`Move to ${k}`}
id={k}
action={() => {
noteHandler.moveNote(note, notebook, k);
updateParent?.();
}}
/>
))}
</Menu.MenuItem>
)}
<Menu.MenuItem
color="danger"
label="Delete Note"
id="delete"
icon={DeleteIcon}
action={() => {
noteHandler.deleteNote(note.id, notebook);
updateParent?.();
}}
/>
</Menu.Menu>
);
}

View file

@ -1,129 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2024 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 "./style.css";
import { NavContextMenuPatchCallback } from "@api/ContextMenu";
import { HeaderBarButton } from "@api/HeaderBar";
import { DataStore } from "@api/index";
import { BookmarkIcon } from "@components/Icons";
import { EquicordDevs } from "@utils/constants";
import { classNameFactory } from "@utils/css";
import { openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { Message } from "@vencord/discord-types";
import { findByCodeLazy, findComponentByCodeLazy, findCssClassesLazy } from "@webpack";
import { ChannelStore, Menu } from "@webpack/common";
import { NoteModal } from "./components/modals/Notebook";
import { noteHandler, noteHandlerCache } from "./NoteHandler";
import { DataStoreToCache, HolyNoteStore } from "./utils";
export const cl = classNameFactory("vc-notebook-");
export const CircleQuestionIcon = findComponentByCodeLazy("10.58l-3.3-3.3a1");
export const MessageRecord = findByCodeLazy("isEdited(){");
export const messageClasses = findCssClassesLazy("message", "groupStart", "cozyMessage");
export const resultsClasses = findCssClassesLazy("emptyResultsWrap", "emptyResultsContent", "errorImage", "emptyResultsText", "noResultsImage", "alt");
export const ChannelRecord = findByCodeLazy("computeLurkerPermissionsAllowList(){");
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => {
children.push(
<Menu.MenuItem label="Note Message" id="note-message">
{Object.keys(noteHandler.getAllNotes()).map(notebook => (
<Menu.MenuItem
key={notebook}
label={notebook}
id={notebook}
action={() => noteHandler.addNote(message, notebook)}
/>
))}
</Menu.MenuItem>
);
};
function ToolBarHeader() {
return (
<HeaderBarButton
tooltip="Holy Notes"
position="bottom"
icon={BookmarkIcon}
onClick={() => openModal(props => <NoteModal {...props} />)}
/>
);
}
export let ChannelMessage;
export default definePlugin({
name: "HolyNotes",
description: "Save messages as notes to revisit later",
dependencies: ["MessagePopoverAPI", "HeaderBarAPI"],
tags: ["Chat", "Organisation"],
authors: [EquicordDevs.Wolfie],
patches: [
{
find: "Message must not be a thread starter message",
replacement: [
{
// Append messagelogger-deleted to classNames if deleted
match: /(?<=(\i)=\i\.memo\(function\(\i\)\{.{0,200}\.THREAD_STARTER_MESSAGE.*?bg-flash-.{0,10}:\i\}\);)/,
replace: "ChannelMessage=$1;"
},
],
}
],
set ChannelMessage(value: any) {
ChannelMessage = value;
},
toolboxActions: {
"Open Notes"() {
openModal(props => <NoteModal {...props} />);
}
},
contextMenus: {
message: messageContextMenuPatch
},
headerBarButton: {
icon: BookmarkIcon,
render: ToolBarHeader
},
messagePopoverButton: {
icon: BookmarkIcon,
render(message) {
return {
label: "Save Note",
icon: BookmarkIcon,
message,
channel: ChannelStore.getChannel(message.channel_id),
onClick: () => noteHandler.addNote(message, "Main")
};
}
},
async start() {
const keys = await DataStore.keys(HolyNoteStore);
if (!keys.includes("Main")) {
noteHandler.newNoteBook("Main");
return;
}
if (!noteHandlerCache.has("Main")) await DataStoreToCache();
},
});

View file

@ -1,120 +0,0 @@
.vc-notebook-header {
align-items: flex-start;
position: relative;
padding: 16px 16px 0 24px;
}
.vc-notebook-header-content {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.vc-notebook-header-trailing {
position: absolute;
top: 8px;
right: 8px;
}
.vc-notebook-title {
color: var(--text-strong);
}
.vc-notebook-description {
color: var(--text-subtle);
}
.vc-notebook-controls {
display: grid;
gap: 10px;
grid-template-columns: 1fr 150px;
padding: 16px 24px;
}
.vc-notebook-tabs {
display: flex;
gap: 24px;
padding: 0 24px;
border-bottom: 1px solid var(--border-subtle);
}
.vc-notebook-tab {
padding: 8px 0;
font-size: 14px;
font-weight: 500;
color: var(--text-subtle);
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.15s ease, border-color 0.15s ease;
}
.vc-notebook-tab:hover {
color: var(--text-default);
border-bottom-color: var(--text-subtle);
}
.vc-notebook-tab-selected {
color: var(--text-default);
border-bottom-color: var(--control-brand-foreground);
}
.vc-notebook-content {
padding: 16px 24px;
}
.vc-notebook-create-modal [class*="footer"] {
gap: 8px;
}
.vc-notebook-icon-button {
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
color: var(--icon-subtle);
border-radius: 4px;
transition: color 0.15s ease;
}
.vc-notebook-icon-button:hover {
color: var(--icon-default);
}
.vc-holy-note [class*="buttonContainer"] {
display: none;
}
.vc-holy-note [class*="messageListItem"] {
list-style-type: none;
}
.vc-holy-note [class*="botText"] {
font-size: 0 !important;
line-height: 0;
}
.vc-holy-note [class*="botText"]::after {
content: "NOTE";
font-size: 0.625rem;
line-height: 1.3;
font-weight: 600;
font-family: var(--font-display);
color: inherit;
letter-spacing: 0.02em;
text-transform: uppercase;
}
.vc-help-markdown {
display: flex;
flex-direction: column;
gap: 4px;
}
.vc-help-markdown hr {
border: none;
height: 1px;
margin: 12px 0;
background-color: var(--border-subtle);
}

View file

@ -1,38 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Embed, MessageAttachment, MessageReaction } from "@vencord/discord-types";
import { StickerFormatType } from "@vencord/discord-types/enums";
export interface NoteAuthor {
id: string;
avatar: string;
discriminator: string;
username: string;
}
export interface NoteStickerItem {
format_type: StickerFormatType;
id: string;
name: string;
}
export interface Note {
id: string;
channel_id: string;
guild_id: string;
content: string;
author: NoteAuthor;
flags: number;
timestamp: string;
attachments: MessageAttachment[];
embeds: Embed[];
reactions: MessageReaction[];
stickerItems: NoteStickerItem[];
}
export type Notebook = Record<string, Note>;
export type AllNotebooks = Record<string, Notebook>;

View file

@ -1,91 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { createStore } from "@api/DataStore";
import { DataStore } from "@api/index";
import { Toasts } from "@webpack/common";
import { noteHandler, noteHandlerCache } from "./NoteHandler";
import { Notebook } from "./types";
export const HolyNoteStore = createStore("HolyNoteData", "HolyNoteStore");
export function saveCacheToDataStore(key: string, value: Notebook) {
DataStore.set(key, value, HolyNoteStore);
}
export function deleteCacheFromDataStore(key: string) {
DataStore.del(key, HolyNoteStore);
}
export async function DataStoreToCache() {
const entries = await DataStore.entries(HolyNoteStore);
for (const [key, value] of entries) {
noteHandlerCache.set(String(key), value as Notebook);
}
}
export async function DeleteEntireStore() {
await DataStore.clear(HolyNoteStore);
noteHandler.newNoteBook("Main", true);
}
export async function downloadNotes() {
const filename = "notes.json";
const data = JSON.stringify(noteHandler.exportNotes(), null, 2);
if (IS_VESKTOP || IS_EQUIBOP || IS_WEB) {
const file = new File([data], filename, { type: "application/json" });
const a = document.createElement("a");
a.href = URL.createObjectURL(file);
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(() => {
URL.revokeObjectURL(a.href);
document.body.removeChild(a);
}, 0);
} else {
DiscordNative.fileManager.saveWithDialog(data, filename);
}
}
export async function uploadNotes() {
if (IS_VESKTOP || IS_EQUIBOP || IS_WEB) {
const input = document.createElement("input");
input.type = "file";
input.accept = "application/json";
input.style.display = "none";
input.onchange = () => {
const file = input.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
try {
noteHandler.importNotes(reader.result as string);
} catch {
Toasts.show({
id: Toasts.genId(),
message: "Failed to import notes",
type: Toasts.Type.FAILURE,
});
}
};
reader.readAsText(file);
};
document.body.appendChild(input);
input.click();
setTimeout(() => document.body.removeChild(input), 0);
} else {
const [file] = await DiscordNative.fileManager.openFiles({
filters: [{ name: "notes", extensions: ["json"] }]
});
if (file) {
noteHandler.importNotes(new TextDecoder().decode(file.data));
}
}
}