diff --git a/src/api/HeaderBar.tsx b/src/api/HeaderBar.tsx index 3be7bc7d0..3b2aa3447 100644 --- a/src/api/HeaderBar.tsx +++ b/src/api/HeaderBar.tsx @@ -98,12 +98,15 @@ export function HeaderBarButton(props: HeaderBarButtonProps & { ref?: React.RefO return ( - {() => ( + {({ onMouseEnter, onMouseLeave }) => ( settings.store.removeButtonTooltips diff --git a/src/equicordplugins/components.dev/components/ComponentsTab.tsx b/src/equicordplugins/components.dev/components/ComponentsTab.tsx index 349328a5f..26ace0405 100644 --- a/src/equicordplugins/components.dev/components/ComponentsTab.tsx +++ b/src/equicordplugins/components.dev/components/ComponentsTab.tsx @@ -40,7 +40,6 @@ import MenuTab from "./tabs/MenuTab"; import ModalTab from "./tabs/ModalTab"; import NoticeTab from "./tabs/NoticeTab"; import PaginatorTab from "./tabs/PaginatorTab"; -import PopoutTab from "./tabs/PopoutTab"; import PopoverTab from "./tabs/PopoverTab"; import ProgressBarTab from "./tabs/ProgressBarTab"; import RadioGroupTab from "./tabs/RadioGroupTab"; @@ -90,7 +89,6 @@ const TABS = [ { id: "modal", label: "Modal" }, { id: "notice", label: "Notice" }, { id: "paginator", label: "Paginator" }, - { id: "popout", label: "Popout" }, { id: "popover", label: "Popover" }, { id: "progressbar", label: "ProgressBar" }, { id: "radiogroup", label: "RadioGroup" }, @@ -144,7 +142,6 @@ const TAB_COMPONENTS: Record = { modal: ModalTab, notice: NoticeTab, paginator: PaginatorTab, - popout: PopoutTab, popover: PopoverTab, progressbar: ProgressBarTab, radiogroup: RadioGroupTab, diff --git a/src/equicordplugins/components.dev/components/index.tsx b/src/equicordplugins/components.dev/components/index.tsx index d4f7c82b0..663f0f649 100644 --- a/src/equicordplugins/components.dev/components/index.tsx +++ b/src/equicordplugins/components.dev/components/index.tsx @@ -75,7 +75,6 @@ import type { NumberBadgeProps, OpenModalOptions, PaginatorProps, - PopoutComponent, ProgressBarProps, SearchBarProps, SimpleErrorBoundaryProps, @@ -150,7 +149,6 @@ export const Accordion = findComponentByCodeLazy("scrollHeight)}", "onExpandedCh export const Timestamp = findComponentByCodeLazy("#{intl::MESSAGE_EDITED_TIMESTAMP_A11Y_LABEL}", "isVisibleOnlyOnHover") as React.ComponentType; export const GuildIcon = findComponentByCodeLazy("Masks.CLAN_ICON,width", "acronymClassName") as React.ComponentType; export const ColorPicker = findComponentByCodeLazy("#{intl::USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR}", "showEyeDropper") as React.ComponentType; -export const Popout: PopoutComponent = proxyLazy(() => findLazy(m => m?.y?.Animation && m.y.toString?.().includes("renderPopout")).y); export const FocusLock = findComponentByCodeLazy(".containerRef,{keyboardModeEnabled:") as React.ComponentType; export const UserSummaryItem = findComponentByCodeLazy("popoutUserId") as React.ComponentType; diff --git a/src/equicordplugins/components.dev/components/tabs/PopoutTab.tsx b/src/equicordplugins/components.dev/components/tabs/PopoutTab.tsx deleted file mode 100644 index 339f7086f..000000000 --- a/src/equicordplugins/components.dev/components/tabs/PopoutTab.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { Clickable, ManaButton, Paragraph, Popout, useState } from ".."; -import { SectionWrapper } from "../SectionWrapper"; - -export default function PopoutTab() { - const [showBasic, setShowBasic] = useState(false); - const [showPositions, setShowPositions] = useState>({}); - - const positions = ["top", "bottom", "left", "right"] as const; - - return ( -
- - - Click the button to toggle a popout. - - setShowBasic(false)} - renderPopout={() => ( -
- This is a popout! - setShowBasic(false)} - style={{ marginTop: 8 }} - /> -
- )} - > - {(props, { isShown }) => ( - - setShowBasic(!showBasic)} - /> - - )} -
-
- - - - Popout can appear in different positions. - -
- {positions.map(position => ( - setShowPositions(p => ({ ...p, [position]: false }))} - renderPopout={() => ( -
- Position: {position} -
- )} - > - {(props, { isShown }) => ( - - setShowPositions(p => ({ ...p, [position]: !p[position] }))} - /> - - )} -
- ))} -
-
- - - - Popout supports different animation types via Popout.Animation. - -
- {(["NONE", "TRANSLATE", "SCALE", "FADE"] as const).map(anim => ( - setShowPositions(p => ({ ...p, [`anim-${anim}`]: false }))} - renderPopout={() => ( -
- Animation: {anim} -
- )} - > - {(props, { isShown }) => ( - - setShowPositions(p => ({ ...p, [`anim-${anim}`]: !p[`anim-${anim}`] }))} - /> - - )} -
- ))} -
-
- - - • children - Render function (props, data) => ReactNode - • renderPopout - Render function for popout content - • shouldShow - Control visibility - • position - top, bottom, left, right, center, window_center - • align - left, right, center, top, bottom - • animation - Popout.Animation.NONE/TRANSLATE/SCALE/FADE - • spacing - Distance from target (default 8) - • onRequestOpen/onRequestClose - Callbacks - • autoInvert - Auto-flip if near edge (default true) - -
- ); -} diff --git a/src/equicordplugins/components.dev/types/index.d.ts b/src/equicordplugins/components.dev/types/index.d.ts index e5dcf1b48..792bf25f5 100644 --- a/src/equicordplugins/components.dev/types/index.d.ts +++ b/src/equicordplugins/components.dev/types/index.d.ts @@ -25,7 +25,6 @@ export * from "./menu"; export * from "./modal"; export * from "./notice"; export * from "./paginator"; -export * from "./popout"; export * from "./progress"; export * from "./radio"; export * from "./scroller"; diff --git a/src/equicordplugins/components.dev/types/popout.d.ts b/src/equicordplugins/components.dev/types/popout.d.ts deleted file mode 100644 index 8f9db0a84..000000000 --- a/src/equicordplugins/components.dev/types/popout.d.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2025 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -export type PopoutAnimationType = "1" | "2" | "3" | "4"; - -export type PopoutPosition = "top" | "bottom" | "left" | "right" | "center" | "window_center"; - -export type PopoutAlign = "left" | "right" | "center" | "top" | "bottom"; - -export interface PopoutChildrenProps { - "aria-controls": string; - "aria-expanded": boolean; - onClick: (event: React.MouseEvent) => void; - onKeyDown: (event: React.KeyboardEvent) => void; - onMouseDown: (event: React.MouseEvent) => void; -} - -export interface PopoutChildrenData { - isShown: boolean; - position: PopoutPosition; -} - -export interface PopoutRenderProps { - closePopout: () => void; - isPositioned: boolean; - nudge: number; - position: PopoutPosition; - setPopoutRef: (ref: any) => void; - updatePosition: () => void; -} - -export interface PopoutProps { - children: (props: PopoutChildrenProps, data: PopoutChildrenData) => React.ReactNode; - renderPopout: (props: PopoutRenderProps) => React.ReactNode; - shouldShow?: boolean; - targetElementRef?: React.RefObject; - onRequestOpen?: () => void; - onRequestClose?: () => void; - align?: PopoutAlign; - animation?: PopoutAnimationType; - animationPosition?: PopoutPosition; - autoInvert?: boolean; - nudgeAlignIntoViewport?: boolean; - position?: PopoutPosition; - positionKey?: string; - popoutKey?: string; - spacing?: number; - preload?: boolean; - fixed?: boolean; - useRawTargetDimensions?: boolean; - onShiftClick?: () => void; - disablePointerEvents?: boolean; - ignoreModalClicks?: boolean; - scrollBehavior?: string; - useMouseEnter?: boolean; - layerContext?: any; - clickTrap?: boolean; -} - -export interface PopoutAnimationEnum { - NONE: "1"; - TRANSLATE: "2"; - SCALE: "3"; - FADE: "4"; -} - -export type PopoutComponent = React.ComponentType & { - Animation: PopoutAnimationEnum; -}; diff --git a/src/equicordplugins/holyNotes/NoteHandler.ts b/src/equicordplugins/holyNotes/NoteHandler.ts index 43bdeb81f..0dcc3ab3e 100644 --- a/src/equicordplugins/holyNotes/NoteHandler.ts +++ b/src/equicordplugins/holyNotes/NoteHandler.ts @@ -4,209 +4,146 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import { makeDummyUser } from "@components/settings/tabs/plugins/PluginModal"; import { Channel, Message } from "@vencord/discord-types"; import { proxyLazyWebpack } from "@webpack"; -import { ChannelStore, lodash, Toasts, UserStore } from "@webpack/common"; +import { ChannelStore, Toasts, UserStore } from "@webpack/common"; -import { Discord, HolyNotes } from "./types"; +import { AllNotebooks, Note, Notebook } from "./types"; import { deleteCacheFromDataStore, DeleteEntireStore, saveCacheToDataStore } from "./utils"; -export const noteHandlerCache = new Map(); +export const noteHandlerCache = new Map(); -export const noteHandler = proxyLazyWebpack(() => { - class NoteHandler { - public _formatNote(channel: Channel, message: Message): HolyNotes.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, - // Moment has a toString() function, this doesn't convert to '[object Object]'. +function showToast(message: string, type: typeof Toasts.Type[keyof typeof Toasts.Type] = Toasts.Type.SUCCESS) { + Toasts.show({ id: Toasts.genId(), message, type }); +} - timestamp: message.timestamp.toString(), - attachments: message.attachments as Discord.Attachment[], - embeds: message.embeds, - reactions: message.reactions as Discord.Reaction[], - stickerItems: message.stickerItems, - }; +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; } - public getNotes(notebook?: string): Record { - return noteHandlerCache.get(notebook); - } + noteHandlerCache.set(name, {}); + saveCacheToDataStore(name, {}); + if (!silent) showToast(`Created notebook ${name}`); + }, - public getAllNotes(): HolyNotes.Note[] { - const data = noteHandlerCache.entries(); - const notes = {}; - for (const [key, value] of data) { - notes[key] = value; - } - return notes as HolyNotes.Note[]; - } + deleteNotebook(name: string) { + noteHandlerCache.delete(name); + deleteCacheFromDataStore(name); + showToast(`Deleted notebook ${name}`); + }, - public addNote = async (message: Message, notebook: string) => { - const notes = this.getNotes(notebook); - const channel = ChannelStore.getChannel(message.channel_id); - const newNotes = Object.assign({ [message.id]: this._formatNote(channel, message) }, notes); + refreshAvatars() { + const notebooks = this.getAllNotes(); - noteHandlerCache.set(notebook, newNotes); - saveCacheToDataStore(notebook, newNotes as unknown as HolyNotes.Note[]); - - Toasts.show({ - id: Toasts.genId(), - message: `Successfully added note to ${notebook}.`, - type: Toasts.Type.SUCCESS, - }); - }; - - public deleteNote = async (noteId: string, notebook: string) => { - const notes = this.getNotes(notebook); - - noteHandlerCache.set(notebook, lodash.omit(notes, noteId)); - saveCacheToDataStore(notebook, lodash.omit(notes, noteId) as unknown as HolyNotes.Note[]); - - Toasts.show({ - id: Toasts.genId(), - message: `Successfully deleted note from ${notebook}.`, - type: Toasts.Type.SUCCESS, - }); - }; - - public moveNote = async (note: HolyNotes.Note, from: string, to: string) => { - const origNotebook = this.getNotes(from); - const newNoteBook = lodash.cloneDeep(this.getNotes(to)); - - newNoteBook[note.id] = note; - - noteHandlerCache.set(from, lodash.omit(origNotebook, note.id)); - noteHandlerCache.set(to, newNoteBook); - - saveCacheToDataStore(from, lodash.omit(origNotebook, note.id) as unknown as HolyNotes.Note[]); - saveCacheToDataStore(to, newNoteBook as unknown as HolyNotes.Note[]); - - Toasts.show({ - id: Toasts.genId(), - message: `Successfully moved note from ${from} to ${to}.`, - type: Toasts.Type.SUCCESS, - }); - }; - - public newNoteBook = async (notebookName: string, silent?: Boolean) => { - let notebookExists = false; - - for (const key of noteHandlerCache.keys()) { - if (key === notebookName) { - notebookExists = true; - break; + 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); + } - if (notebookExists) { - Toasts.show({ - id: Toasts.genId(), - message: `Notebook ${notebookName} already exists.`, - type: Toasts.Type.FAILURE, - }); - return; - } + showToast("Refreshed avatars"); + }, - noteHandlerCache.set(notebookName, {}); - saveCacheToDataStore(notebookName, {} as HolyNotes.Note[]); + async deleteEverything() { + noteHandlerCache.clear(); + await DeleteEntireStore(); + showToast("Deleted all notes"); + }, - if (!silent) return Toasts.show({ - id: Toasts.genId(), - message: `Successfully created ${notebookName}.`, - type: Toasts.Type.SUCCESS, - }); - }; + exportNotes(): AllNotebooks { + return this.getAllNotes(); + }, - public deleteNotebook = async (notebookName: string) => { - noteHandlerCache.delete(notebookName); - deleteCacheFromDataStore(notebookName); + importNotes(jsonString: string) { + let parsed: AllNotebooks; + try { + parsed = JSON.parse(jsonString); + } catch { + showToast("Invalid JSON", Toasts.Type.FAILURE); + return; + } - Toasts.show({ - id: Toasts.genId(), - message: `Successfully deleted ${notebookName}.`, - type: Toasts.Type.SUCCESS, - }); - }; + for (const notebook in parsed) { + noteHandlerCache.set(notebook, parsed[notebook]); + saveCacheToDataStore(notebook, parsed[notebook]); + } - public refreshAvatars = async () => { - const notebooks = this.getAllNotes(); - - for (const notebook in notebooks) - for (const noteId in notebooks[notebook]) { - const note = notebooks[notebook][noteId]; - const user = UserStore.getUser(note.author.id) ?? makeDummyUser(note.author); - - Object.assign(notebooks[notebook][noteId].author, { - avatar: user.avatar, - discriminator: user.discriminator, - username: user.username, - }); - } - - for (const notebook in notebooks) { - noteHandlerCache.set(notebook, notebooks[notebook]); - saveCacheToDataStore(notebook, notebooks[notebook] as unknown as HolyNotes.Note[]); - } - - Toasts.show({ - id: Toasts.genId(), - message: "Successfully refreshed avatars.", - type: Toasts.Type.SUCCESS, - }); - - }; - - public deleteEverything = async () => { - noteHandlerCache.clear(); - await DeleteEntireStore(); - - Toasts.show({ - id: Toasts.genId(), - message: "Successfully deleted all notes.", - type: Toasts.Type.SUCCESS, - }); - }; - - public exportNotes = async () => { - return this.getAllNotes(); - }; - - public importNotes = async (notes: HolyNotes.Note[]) => { - try { - var parseNotes = JSON.parse(notes as unknown as string); - } catch (e) { - console.log(e); - return Toasts.show({ - id: Toasts.genId(), - message: "Invalid JSON.", - type: Toasts.Type.FAILURE, - }); - } - - for (const notebook in parseNotes) { - noteHandlerCache.set(notebook, parseNotes[notebook]); - saveCacheToDataStore(notebook, parseNotes[notebook] as unknown as HolyNotes.Note[]); - } - - Toasts.show({ - id: Toasts.genId(), - message: "Successfully imported notes.", - type: Toasts.Type.SUCCESS, - }); - - }; - } - - return new NoteHandler(); -}); + showToast("Imported notes"); + }, +})); diff --git a/src/equicordplugins/holyNotes/components/icons/HelpIcon.tsx b/src/equicordplugins/holyNotes/components/icons/HelpIcon.tsx deleted file mode 100644 index 492291ad3..000000000 --- a/src/equicordplugins/holyNotes/components/icons/HelpIcon.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { classes } from "@utils/misc"; -import { JSX } from "react"; - -export default ({ className }: { className?: string; }): JSX.Element => ( - -); diff --git a/src/equicordplugins/holyNotes/components/icons/NoteButton.tsx b/src/equicordplugins/holyNotes/components/icons/NoteButton.tsx deleted file mode 100644 index b0b823bfa..000000000 --- a/src/equicordplugins/holyNotes/components/icons/NoteButton.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { JSX } from "react"; - -const component = (props: React.SVGProps): JSX.Element => ( - )} - > - - -); - -export default component; -export const Popover = component as unknown as (props: unknown) => JSX.Element; diff --git a/src/equicordplugins/holyNotes/components/icons/overFlowIcon.tsx b/src/equicordplugins/holyNotes/components/icons/overFlowIcon.tsx deleted file mode 100644 index 24f5c9185..000000000 --- a/src/equicordplugins/holyNotes/components/icons/overFlowIcon.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { JSX } from "react"; - -const OverFlowIcon = ({ width = 24, height = 24, color = "var(--interactive-icon-default)", className, ...rest }) => ( - - - - -); - -export default OverFlowIcon; -export const SvgOverFlowIcon = OverFlowIcon as unknown as (props: React.SVGProps) => JSX.Element; diff --git a/src/equicordplugins/holyNotes/components/modals/Error.tsx b/src/equicordplugins/holyNotes/components/modals/Error.tsx index 06d0ba671..df7974d71 100644 --- a/src/equicordplugins/holyNotes/components/modals/Error.tsx +++ b/src/equicordplugins/holyNotes/components/modals/Error.tsx @@ -6,43 +6,33 @@ import { resultsClasses } from "@equicordplugins/holyNotes"; -export default ({ error }: { error?: Error; } = {}) => { +export default function EmptyNotebook({ error }: { error?: Error; } = {}) { if (error) { - // Error - console.log(error); + console.error("[HolyNotes] Error:", error); return (
-
+
- There was an error parsing your notes! The issue was logged in your console, press CTRL - + I to access it! Please visit the support server if you need extra help! + There was an error parsing your notes. Check the console for details.
); - } else if (Math.floor(Math.random() * 100) <= 10) - // Easter egg - return ( -
-
-
-
- No notes were found. Empathy banana is here for you. -
+ } + + const isEasterEgg = Math.random() < 0.1; + + return ( +
+
+
+
+ {isEasterEgg + ? "No notes were found. Empathy banana is here for you." + : "No notes were found in this notebook."}
- ); - // Empty notebook - else - return ( -
-
-
-
- No notes were found saved in this notebook. -
-
-
- ); -}; +
+ ); +} diff --git a/src/equicordplugins/holyNotes/components/modals/HelpModal.tsx b/src/equicordplugins/holyNotes/components/modals/HelpModal.tsx index af1fae928..3efe060c2 100644 --- a/src/equicordplugins/holyNotes/components/modals/HelpModal.tsx +++ b/src/equicordplugins/holyNotes/components/modals/HelpModal.tsx @@ -5,79 +5,71 @@ */ import { BaseText } from "@components/BaseText"; -import { Paragraph } from "@components/Paragraph"; +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 { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal"; -import { Button } from "@webpack/common"; +import { CloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize } from "@utils/modal"; -export default ({ onClose, ...modalProps }: ModalProps & { onClose: () => void; }) => { +function HelpSection({ title, children }: { title: string; children: React.ReactNode; }) { return ( - - - Help Modal - + <> + {title} + {children} +
+ + ); +} + +export default function HelpModal({ onClose, transitionState }: ModalProps) { + return ( + + +
+ + Help + + + Learn how to use Holy Notes + +
+
+ +
- +
- Adding Notes - - To add a note right click on a message then hover over the "Note Message" item and click the button with the notebook name you would like to note the message to. - -
- Protip: -
- - Clicking the "Note Message" button by itself will note to Main by default! - -
- Deleting Notes - - Note you can either right click the note and hit "Delete Note" or you can hold the - 'DELETE' key on your keyboard and click on a note; it's like magic! - -
- Moving Notes - - To move a note right click on a note and hover over the "Move Note" item and click on - the button corresponding to the notebook you would like to move the note to. - -
- Jump To Message - - To jump to the location that the note was originally located at just right click on the - note and hit "Jump to Message". - + + Right-click a message, hover over "Note Message", and select the notebook. + + + Right-click a note and select "Delete Note", or hold DELETE and click a note. + + + Right-click a note, hover over "Move Note", and select the destination notebook. + + + Right-click a note and select "Jump to Message" to go to the original message. +
-
- - - - -
+ + + + + +
); -}; +} diff --git a/src/equicordplugins/holyNotes/components/modals/ManageNotebookButton.tsx b/src/equicordplugins/holyNotes/components/modals/ManageNotebookButton.tsx deleted file mode 100644 index 7e30793c6..000000000 --- a/src/equicordplugins/holyNotes/components/modals/ManageNotebookButton.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { openModal } from "@utils/modal"; -import { Button, React } from "@webpack/common"; - -import NotebookCreateModal from "./NotebookCreateModal"; -import NotebookDeleteModal from "./NotebookDeleteModal"; - -export default ({ notebook, setNotebook }: { notebook: string, setNotebook: React.Dispatch>; }) => { - const isNotMain = notebook !== "Main"; - - return ( - - ); -}; diff --git a/src/equicordplugins/holyNotes/components/modals/NoteBookTab.tsx b/src/equicordplugins/holyNotes/components/modals/NoteBookTab.tsx deleted file mode 100644 index 00da35c6f..000000000 --- a/src/equicordplugins/holyNotes/components/modals/NoteBookTab.tsx +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { SvgOverFlowIcon } from "@equicordplugins/holyNotes/components/icons/overFlowIcon"; -import { classes } from "@utils/misc"; -import { findCssClassesLazy } from "@webpack"; -import { Button, Clickable, Menu, Popout, React, useRef } from "@webpack/common"; - -const { overflowIcon } = findCssClassesLazy("overflowIcon", "forceButtonsShow"); - -export function NoteBookTabs({ tabs, selectedTabId, onSelectTab }: { tabs: string[], selectedTabId: string, onSelectTab: (tab: string) => void; }) { - const tabBarRef = React.useRef(null); - const widthRef = React.useRef(0); - const tabWidthMapRef = React.useRef(new Map()); - const [overflowedTabs, setOverflowedTabs] = React.useState([]); - const resizeObserverRef = React.useRef(null); - const [show, setShow] = React.useState(false); - - function isNotNullish(value) { - return value !== null && value !== undefined; - } - - const handleResize = React.useCallback(() => { - if (!tabBarRef.current) return; - const overflowed = [] as string[]; - - const totalWidth = tabBarRef.current.clientWidth; - if (totalWidth !== widthRef.current) { - - // Thanks to daveyy1 for the help with this - let width = 0; - for (let i = 0; i < tabs.length; i++) { - const tab = tabs[i]; - const tabRef = tabWidthMapRef.current.get(tab); - - if (!tabRef) continue; - width += tabRef.width; - - if (width > totalWidth) { - overflowed.push(tab); - } - - } - - setOverflowedTabs(overflowed); - } - }, [tabs, selectedTabId]); - - React.useEffect(() => { - handleResize(); - - resizeObserverRef.current = new ResizeObserver(handleResize); - - if (tabBarRef.current) resizeObserverRef.current.observe(tabBarRef.current); - return () => { - if (resizeObserverRef.current) resizeObserverRef.current.disconnect(); - }; - }, [handleResize]); - - const TabItem = React.forwardRef(function ({ id, selected, onClick, children }: { id: string, selected: boolean, onClick: () => void, children: React.ReactNode; }, ref) { - return ( - - {children} - - ); - }); - - const renderOverflowMenu = React.useCallback((closePopout: () => void) => { - return ( - - {tabs.map(tab => { - return overflowedTabs.includes(tab) && selectedTabId !== tab ? ( - onSelectTab(tab)} - /> - ) : null; - }).filter(isNotNullish)} - - - ); - }, [tabs, selectedTabId, onSelectTab, overflowedTabs]); - - const buttonRef = useRef(null); - - return ( -
- - {tabs.map(tab => { - if (!overflowedTabs.includes(tab)) { - return ( - { - const width = tabWidthMapRef.current.get(tab)?.width ?? 0; - tabWidthMapRef.current.set(tab, { - node: el, - width: el ? el.getBoundingClientRect().width : width - }); - }} - onClick={selectedTabId !== tab ? () => onSelectTab(tab) : () => { }} - > - {tab} - - ); - } - return null; - }).filter(isNotNullish)} - {overflowedTabs.length > 0 && ( - setShow(false)} - renderPopout={() => renderOverflowMenu(() => setShow(false))} - position="bottom" - align="right" - spacing={0} - targetElementRef={buttonRef} - > - {props => ( - - )} - - - )} -
- ); -} - -export function CreateTabBar({ tabs, firstSelectedTab, onChangeTab }) { - const tabKeys = Object.keys(tabs); - const mainTabIndex = tabKeys.indexOf("Main"); - if (mainTabIndex !== -1 && mainTabIndex !== 0) { - tabKeys.splice(mainTabIndex, 1); - tabKeys.unshift("Main"); - } - - const [selectedTab, setSelectedTab] = React.useState( - firstSelectedTab || (tabKeys.length > 0 ? tabKeys[0] : null) - ); - - const renderSelectedTab = React.useCallback(() => { - const selectedTabId = tabKeys.find(tab => tab === selectedTab); - return selectedTabId; - }, [tabs, selectedTab]); - - return { - TabBar: { - setSelectedTab(tab); - if (onChangeTab) onChangeTab(tab); - }} />, - renderSelectedTab, - selectedTab - }; -} diff --git a/src/equicordplugins/holyNotes/components/modals/Notebook.tsx b/src/equicordplugins/holyNotes/components/modals/Notebook.tsx index 9b9dd4d15..011ccbf43 100644 --- a/src/equicordplugins/holyNotes/components/modals/Notebook.tsx +++ b/src/equicordplugins/holyNotes/components/modals/Notebook.tsx @@ -5,261 +5,172 @@ */ import { BaseText } from "@components/BaseText"; +import { Button } from "@components/Button"; import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; -import { quickSelectClasses } from "@equicordplugins/holyNotes"; -import HelpIcon from "@equicordplugins/holyNotes/components/icons/HelpIcon"; -import NoteButton from "@equicordplugins/holyNotes/components/icons/NoteButton"; +import { BookmarkIcon, CircleQuestionIcon, cl } from "@equicordplugins/holyNotes"; import { noteHandler } from "@equicordplugins/holyNotes/NoteHandler"; -import { HolyNotes } from "@equicordplugins/holyNotes/types"; -import { classes } from "@utils/misc"; -import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal"; -import { ContextMenuApi, FluxDispatcher, Menu, React, TextInput } from "@webpack/common"; +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 ManageNotebookButton from "./ManageNotebookButton"; -import { CreateTabBar } from "./NoteBookTab"; +import NotebookCreateModal from "./NotebookCreateModal"; +import NotebookDeleteModal from "./NotebookDeleteModal"; import { RenderMessage } from "./RenderMessage"; -const renderNotebook = ({ - notes, notebook, updateParent, sortDirection, sortType, searchInput, closeModal -}: { - notes: Record; - notebook: string; - updateParent: () => void; - sortDirection: boolean; - sortType: boolean; - searchInput: string; - closeModal: () => void; -}) => { - let notesArray = Object.values(notes); +const enum SortOption { + NewestAdded, + OldestAdded, + NewestMessage, + OldestMessage +} - if (searchInput) { - const searchLower = searchInput.toLowerCase(); - notesArray = notesArray.filter(note => - note.content?.toLowerCase().includes(searchLower) - ); - } - - if (!notesArray.length) return ; - - if (sortType) { - notesArray.sort((a, b) => - new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() - ); - } - - if (sortDirection) notesArray.reverse(); - - return notesArray.map(note => ( - - )); -}; - -export const NoteModal = (props: ModalProps & { onClose: () => void; }) => { - const [sortType, setSortType] = React.useState(true); - const [searchInput, setSearch] = React.useState(""); - const [sortDirection, setSortDirection] = React.useState(true); - const [currentNotebook, setCurrentNotebook] = React.useState("Main"); - - const forceUpdate = React.useReducer(() => ({}), {})[1] as () => void; - - const notes = noteHandler.getNotes(currentNotebook); - if (!notes) return <>; - - const notesArray = Object.values(notes); - const noteCount = notesArray.length; - - // Filter notes for display count - let filteredCount = noteCount; - if (searchInput) { - const searchLower = searchInput.toLowerCase(); - filteredCount = notesArray.filter(note => - note.content?.toLowerCase().includes(searchLower) - ).length; - } - - const { TabBar, selectedTab } = CreateTabBar({ - tabs: noteHandler.getAllNotes(), - firstSelectedTab: currentNotebook, - onChangeTab: setCurrentNotebook - }); +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 ( - - - -
- - - - - - Notebook - - - {searchInput - ? `${filteredCount} of ${noteCount} ${noteCount === 1 ? "note" : "notes"}` - : `${noteCount} ${noteCount === 1 ? "note" : "notes"}` - } - - - - -
openModal(HelpModal)} - role="button" - tabIndex={0} - aria-label="Help" - onKeyDown={e => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - openModal(HelpModal); - } - }}> - -
- -
-
-
- setSearch(e)} - className={classes("vc-notebook-search-input")} - /> -
-
- {TabBar} -
-
- - - {renderNotebook({ - notes, - notebook: currentNotebook, - updateParent: () => forceUpdate(), - sortDirection: sortDirection, - sortType: sortType, - searchInput: searchInput, - closeModal: props.onClose, - })} - - -
- - - -
- ) => { - ContextMenuApi.openContextMenu(event, () => ( - FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })} - aria-label="Sort options" - > - { - setSortDirection(true); - setSortType(true); - }} - /> - { - setSortDirection(true); - setSortType(false); - }} - /> - { - setSortDirection(false); - setSortType(true); - }} - /> - { - setSortDirection(false); - setSortType(false); - }} - /> - - )); - }} - onKeyDown={e => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - const el = e.currentTarget as HTMLElement; - const rect = el.getBoundingClientRect(); - const evt = { - clientX: rect.left + rect.width / 2, - clientY: rect.top + rect.height / 2 - } as any; - - ContextMenuApi.openContextMenu(evt, () => ( - FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })} - aria-label="Sort options" - > - { setSortDirection(true); setSortType(true); }} /> - { setSortDirection(true); setSortType(false); }} /> - { setSortDirection(false); setSortType(true); }} /> - { setSortDirection(false); setSortType(false); }} /> - - )); - } - }} - > - Sort: - - - {sortDirection ? "Asc" : "Desc"} /{" "} - {sortType ? "Date Added" : "Msg Date"} - - - - - - +
+ {sorted.map(name => ( + onSelect(name)} + > + {name} + + ))} +
); -}; +} + +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 ( + + +
+ + + + Notebook + + + + {search ? `${filtered.length} of ${total}` : total} {total === 1 ? "note" : "notes"} + +
+
+ +
+
+ +
+ +