HolyNotes: Removed
This commit is contained in:
parent
0397b4a2df
commit
6900c60b5b
11 changed files with 0 additions and 1120 deletions
|
|
@ -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");
|
||||
},
|
||||
}));
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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();
|
||||
},
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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>;
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue