From a73d09a2f0d2adc7ff56e6f6004cd6ec50e202e9 Mon Sep 17 00:00:00 2001 From: Syncx <47534062+Syncxv@users.noreply.github.com> Date: Wed, 13 Sep 2023 07:14:17 +1000 Subject: PreviewMessage: Add attachments (& misc changes) (#1715) --- src/plugins/favEmojiFirst.ts | 83 ------------ src/plugins/favEmojiFirst/README.md | 6 + src/plugins/favEmojiFirst/index.ts | 83 ++++++++++++ src/plugins/favGifSearch.tsx | 241 ----------------------------------- src/plugins/favGifSearch/README.md | 5 + src/plugins/favGifSearch/index.tsx | 241 +++++++++++++++++++++++++++++++++++ src/plugins/imageZoom/README.md | 6 + src/plugins/imageZoom/index.tsx | 9 ++ src/plugins/previewMessage.tsx | 82 ------------ src/plugins/previewMessage/README.md | 5 + src/plugins/previewMessage/index.tsx | 141 ++++++++++++++++++++ src/plugins/searchReply.tsx | 79 ------------ src/plugins/searchReply/README.md | 5 + src/plugins/searchReply/index.tsx | 79 ++++++++++++ src/webpack/common/types/menu.d.ts | 9 +- 15 files changed, 587 insertions(+), 487 deletions(-) delete mode 100644 src/plugins/favEmojiFirst.ts create mode 100644 src/plugins/favEmojiFirst/README.md create mode 100644 src/plugins/favEmojiFirst/index.ts delete mode 100644 src/plugins/favGifSearch.tsx create mode 100644 src/plugins/favGifSearch/README.md create mode 100644 src/plugins/favGifSearch/index.tsx create mode 100644 src/plugins/imageZoom/README.md delete mode 100644 src/plugins/previewMessage.tsx create mode 100644 src/plugins/previewMessage/README.md create mode 100644 src/plugins/previewMessage/index.tsx delete mode 100644 src/plugins/searchReply.tsx create mode 100644 src/plugins/searchReply/README.md create mode 100644 src/plugins/searchReply/index.tsx (limited to 'src') diff --git a/src/plugins/favEmojiFirst.ts b/src/plugins/favEmojiFirst.ts deleted file mode 100644 index fec0b04..0000000 --- a/src/plugins/favEmojiFirst.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2023 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { EmojiStore } from "@webpack/common"; -import { Emoji } from "@webpack/types"; - -interface EmojiAutocompleteState { - query?: { - type: string; - typeInfo: { - sentinel: string; - }; - results: { - emojis: Emoji[] & { sliceTo?: number; }; - }; - }; -} - -export default definePlugin({ - name: "FavoriteEmojiFirst", - authors: [Devs.Aria, Devs.Ven], - description: "Puts your favorite emoji first in the emoji autocomplete.", - patches: [ - { - find: ".activeCommandOption", - replacement: [ - { - // = someFunc(a.selectedIndex); ...trackEmojiSearch({ state: theState, isInPopoutExperimental: someBool }) - match: /=\i\(\i\.selectedIndex\);(?=.+?state:(\i),isInPopoutExperiment:\i)/, - // self.sortEmojis(theState) - replace: "$&$self.sortEmojis($1);" - }, - - // set maxCount to Infinity so our sortEmojis callback gets the entire list, not just the first 10 - // and remove Discord's emojiResult slice, storing the endIndex on the array for us to use later - { - // searchEmojis(...,maxCount: stuff) ... endEmojis = emojis.slice(0, maxCount - gifResults.length) - match: /,maxCount:(\i)(.+?)=(\i)\.slice\(0,(\1-\i\.length)\)/, - // ,maxCount:Infinity ... endEmojis = (emojis.sliceTo = n, emojis) - replace: ",maxCount:Infinity$2=($3.sliceTo=$4,$3)" - } - ] - } - ], - - sortEmojis({ query }: EmojiAutocompleteState) { - if ( - query?.type !== "EMOJIS_AND_STICKERS" - || query.typeInfo?.sentinel !== ":" - || !query.results?.emojis?.length - ) return; - - const emojiContext = EmojiStore.getDisambiguatedEmojiContext(); - - query.results.emojis = query.results.emojis.sort((a, b) => { - const aIsFavorite = emojiContext.isFavoriteEmojiWithoutFetchingLatest(a); - const bIsFavorite = emojiContext.isFavoriteEmojiWithoutFetchingLatest(b); - - if (aIsFavorite && !bIsFavorite) return -1; - - if (!aIsFavorite && bIsFavorite) return 1; - - return 0; - }).slice(0, query.results.emojis.sliceTo ?? 10); - } -}); diff --git a/src/plugins/favEmojiFirst/README.md b/src/plugins/favEmojiFirst/README.md new file mode 100644 index 0000000..dc84480 --- /dev/null +++ b/src/plugins/favEmojiFirst/README.md @@ -0,0 +1,6 @@ +# FavoriteEmojiFirst + +Puts your favorite emoji first in the emoji autocomplete. + +![FavEmojis](https://i.imgur.com/mEFCoZG.png) +![Example](https://i.imgur.com/wY3Tc43.png) diff --git a/src/plugins/favEmojiFirst/index.ts b/src/plugins/favEmojiFirst/index.ts new file mode 100644 index 0000000..fec0b04 --- /dev/null +++ b/src/plugins/favEmojiFirst/index.ts @@ -0,0 +1,83 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { EmojiStore } from "@webpack/common"; +import { Emoji } from "@webpack/types"; + +interface EmojiAutocompleteState { + query?: { + type: string; + typeInfo: { + sentinel: string; + }; + results: { + emojis: Emoji[] & { sliceTo?: number; }; + }; + }; +} + +export default definePlugin({ + name: "FavoriteEmojiFirst", + authors: [Devs.Aria, Devs.Ven], + description: "Puts your favorite emoji first in the emoji autocomplete.", + patches: [ + { + find: ".activeCommandOption", + replacement: [ + { + // = someFunc(a.selectedIndex); ...trackEmojiSearch({ state: theState, isInPopoutExperimental: someBool }) + match: /=\i\(\i\.selectedIndex\);(?=.+?state:(\i),isInPopoutExperiment:\i)/, + // self.sortEmojis(theState) + replace: "$&$self.sortEmojis($1);" + }, + + // set maxCount to Infinity so our sortEmojis callback gets the entire list, not just the first 10 + // and remove Discord's emojiResult slice, storing the endIndex on the array for us to use later + { + // searchEmojis(...,maxCount: stuff) ... endEmojis = emojis.slice(0, maxCount - gifResults.length) + match: /,maxCount:(\i)(.+?)=(\i)\.slice\(0,(\1-\i\.length)\)/, + // ,maxCount:Infinity ... endEmojis = (emojis.sliceTo = n, emojis) + replace: ",maxCount:Infinity$2=($3.sliceTo=$4,$3)" + } + ] + } + ], + + sortEmojis({ query }: EmojiAutocompleteState) { + if ( + query?.type !== "EMOJIS_AND_STICKERS" + || query.typeInfo?.sentinel !== ":" + || !query.results?.emojis?.length + ) return; + + const emojiContext = EmojiStore.getDisambiguatedEmojiContext(); + + query.results.emojis = query.results.emojis.sort((a, b) => { + const aIsFavorite = emojiContext.isFavoriteEmojiWithoutFetchingLatest(a); + const bIsFavorite = emojiContext.isFavoriteEmojiWithoutFetchingLatest(b); + + if (aIsFavorite && !bIsFavorite) return -1; + + if (!aIsFavorite && bIsFavorite) return 1; + + return 0; + }).slice(0, query.results.emojis.sliceTo ?? 10); + } +}); diff --git a/src/plugins/favGifSearch.tsx b/src/plugins/favGifSearch.tsx deleted file mode 100644 index db575a0..0000000 --- a/src/plugins/favGifSearch.tsx +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2023 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { definePluginSettings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { findByPropsLazy } from "@webpack"; -import { useCallback, useEffect, useRef, useState } from "@webpack/common"; - -interface SearchBarComponentProps { - ref?: React.MutableRefObject; - autoFocus: boolean; - className: string; - size: string; - onChange: (query: string) => void; - onClear: () => void; - query: string; - placeholder: string; -} - -type TSearchBarComponent = - React.FC & { Sizes: Record<"SMALL" | "MEDIUM" | "LARGE", string>; }; - -interface Gif { - format: number; - src: string; - width: number; - height: number; - order: number; - url: string; -} - -interface Instance { - dead?: boolean; - state: { - resultType?: string; - }; - props: { - favCopy: Gif[], - - favorites: Gif[], - }, - forceUpdate: () => void; -} - - -const containerClasses: { searchBar: string; } = findByPropsLazy("searchBar", "searchHeader", "gutterSize"); - -export const settings = definePluginSettings({ - searchOption: { - type: OptionType.SELECT, - description: "The part of the url you want to search", - options: [ - { - label: "Entire Url", - value: "url" - }, - { - label: "Path Only (/somegif.gif)", - value: "path" - }, - { - label: "Host & Path (tenor.com somgif.gif)", - value: "hostandpath", - default: true - } - ] as const - } -}); - -export default definePlugin({ - name: "FavoriteGifSearch", - authors: [Devs.Aria], - description: "Adds a search bar for favorite gifs", - - patches: [ - { - find: "renderCategoryExtras", - replacement: [ - { - // https://regex101.com/r/4uHtTE/1 - // ($1 renderHeaderContent=function { ... switch (x) ... case FAVORITES:return) ($2) ($3 case default:return r.jsx(($), {...props})) - match: /(renderHeaderContent=function.{1,150}FAVORITES:return)(.{1,150};)(case.{1,200}default:return\(0,\i\.jsx\)\((?\i\.\i))/, - replace: "$1 this.state.resultType === \"Favorites\" ? $self.renderSearchBar(this, $) : $2; $3" - }, - { - // to persist filtered favorites when component re-renders. - // when resizing the window the component rerenders and we loose the filtered favorites and have to type in the search bar to get them again - match: /(,suggestions:\i,favorites:)(\i),/, - replace: "$1$self.getFav($2),favCopy:$2," - } - - ] - } - ], - - settings, - - getTargetString, - - instance: null as Instance | null, - renderSearchBar(instance: Instance, SearchBarComponent: TSearchBarComponent) { - this.instance = instance; - return ( - - - - ); - }, - - getFav(favorites: Gif[]) { - if (!this.instance || this.instance.dead) return favorites; - const { favorites: filteredFavorites } = this.instance.props; - - return filteredFavorites != null && filteredFavorites?.length !== favorites.length ? filteredFavorites : favorites; - - } -}); - - -function SearchBar({ instance, SearchBarComponent }: { instance: Instance; SearchBarComponent: TSearchBarComponent; }) { - const [query, setQuery] = useState(""); - const ref = useRef<{ containerRef?: React.MutableRefObject; } | null>(null); - - const onChange = useCallback((searchQuery: string) => { - setQuery(searchQuery); - const { props } = instance; - - // return early - if (searchQuery === "") { - props.favorites = props.favCopy; - instance.forceUpdate(); - return; - } - - - // scroll back to top - ref.current?.containerRef?.current - .closest("#gif-picker-tab-panel") - ?.querySelector("[class|=\"content\"]") - ?.firstElementChild?.scrollTo(0, 0); - - - const result = - props.favCopy - .map(gif => ({ - score: fuzzySearch(searchQuery.toLowerCase(), getTargetString(gif.url ?? gif.src).replace(/(%20|[_-])/g, " ").toLowerCase()), - gif, - })) - .filter(m => m.score != null) as { score: number; gif: Gif; }[]; - - result.sort((a, b) => b.score - a.score); - props.favorites = result.map(e => e.gif); - - instance.forceUpdate(); - }, [instance.state]); - - useEffect(() => { - return () => { - instance.dead = true; - }; - }, []); - - return ( - { - setQuery(""); - if (instance.props.favCopy != null) { - instance.props.favorites = instance.props.favCopy; - instance.forceUpdate(); - } - }} - query={query} - placeholder="Search Favorite Gifs" - /> - ); -} - - - -export function getTargetString(urlStr: string) { - const url = new URL(urlStr); - switch (settings.store.searchOption) { - case "url": - return url.href; - case "path": - if (url.host === "media.discordapp.net" || url.host === "tenor.com") - // /attachments/899763415290097664/1095711736461537381/attachment-1.gif -> attachment-1.gif - // /view/some-gif-hi-24248063 -> some-gif-hi-24248063 - return url.pathname.split("/").at(-1) ?? url.pathname; - return url.pathname; - case "hostandpath": - if (url.host === "media.discordapp.net" || url.host === "tenor.com") - return `${url.host} ${url.pathname.split("/").at(-1) ?? url.pathname}`; - return `${url.host} ${url.pathname}`; - - default: - return ""; - } -} - -function fuzzySearch(searchQuery: string, searchString: string) { - let searchIndex = 0; - let score = 0; - - for (let i = 0; i < searchString.length; i++) { - if (searchString[i] === searchQuery[searchIndex]) { - score++; - searchIndex++; - } else { - score--; - } - - if (searchIndex === searchQuery.length) { - return score; - } - } - - return null; -} diff --git a/src/plugins/favGifSearch/README.md b/src/plugins/favGifSearch/README.md new file mode 100644 index 0000000..c076885 --- /dev/null +++ b/src/plugins/favGifSearch/README.md @@ -0,0 +1,5 @@ +# FavoriteGifSearch + +Adds a search bar to favorite gifs. + +![Screenshot](https://i.imgur.com/Bcgb7PD.png) diff --git a/src/plugins/favGifSearch/index.tsx b/src/plugins/favGifSearch/index.tsx new file mode 100644 index 0000000..d10c515 --- /dev/null +++ b/src/plugins/favGifSearch/index.tsx @@ -0,0 +1,241 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { definePluginSettings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { useCallback, useEffect, useRef, useState } from "@webpack/common"; + +interface SearchBarComponentProps { + ref?: React.MutableRefObject; + autoFocus: boolean; + className: string; + size: string; + onChange: (query: string) => void; + onClear: () => void; + query: string; + placeholder: string; +} + +type TSearchBarComponent = + React.FC & { Sizes: Record<"SMALL" | "MEDIUM" | "LARGE", string>; }; + +interface Gif { + format: number; + src: string; + width: number; + height: number; + order: number; + url: string; +} + +interface Instance { + dead?: boolean; + state: { + resultType?: string; + }; + props: { + favCopy: Gif[], + + favorites: Gif[], + }, + forceUpdate: () => void; +} + + +const containerClasses: { searchBar: string; } = findByPropsLazy("searchBar", "searchHeader", "gutterSize"); + +export const settings = definePluginSettings({ + searchOption: { + type: OptionType.SELECT, + description: "The part of the url you want to search", + options: [ + { + label: "Entire Url", + value: "url" + }, + { + label: "Path Only (/somegif.gif)", + value: "path" + }, + { + label: "Host & Path (tenor.com somgif.gif)", + value: "hostandpath", + default: true + } + ] as const + } +}); + +export default definePlugin({ + name: "FavoriteGifSearch", + authors: [Devs.Aria], + description: "Adds a search bar to favorite gifs.", + + patches: [ + { + find: "renderCategoryExtras", + replacement: [ + { + // https://regex101.com/r/4uHtTE/1 + // ($1 renderHeaderContent=function { ... switch (x) ... case FAVORITES:return) ($2) ($3 case default:return r.jsx(($), {...props})) + match: /(renderHeaderContent=function.{1,150}FAVORITES:return)(.{1,150};)(case.{1,200}default:return\(0,\i\.jsx\)\((?\i\.\i))/, + replace: "$1 this.state.resultType === \"Favorites\" ? $self.renderSearchBar(this, $) : $2; $3" + }, + { + // to persist filtered favorites when component re-renders. + // when resizing the window the component rerenders and we loose the filtered favorites and have to type in the search bar to get them again + match: /(,suggestions:\i,favorites:)(\i),/, + replace: "$1$self.getFav($2),favCopy:$2," + } + + ] + } + ], + + settings, + + getTargetString, + + instance: null as Instance | null, + renderSearchBar(instance: Instance, SearchBarComponent: TSearchBarComponent) { + this.instance = instance; + return ( + + + + ); + }, + + getFav(favorites: Gif[]) { + if (!this.instance || this.instance.dead) return favorites; + const { favorites: filteredFavorites } = this.instance.props; + + return filteredFavorites != null && filteredFavorites?.length !== favorites.length ? filteredFavorites : favorites; + + } +}); + + +function SearchBar({ instance, SearchBarComponent }: { instance: Instance; SearchBarComponent: TSearchBarComponent; }) { + const [query, setQuery] = useState(""); + const ref = useRef<{ containerRef?: React.MutableRefObject; } | null>(null); + + const onChange = useCallback((searchQuery: string) => { + setQuery(searchQuery); + const { props } = instance; + + // return early + if (searchQuery === "") { + props.favorites = props.favCopy; + instance.forceUpdate(); + return; + } + + + // scroll back to top + ref.current?.containerRef?.current + .closest("#gif-picker-tab-panel") + ?.querySelector("[class|=\"content\"]") + ?.firstElementChild?.scrollTo(0, 0); + + + const result = + props.favCopy + .map(gif => ({ + score: fuzzySearch(searchQuery.toLowerCase(), getTargetString(gif.url ?? gif.src).replace(/(%20|[_-])/g, " ").toLowerCase()), + gif, + })) + .filter(m => m.score != null) as { score: number; gif: Gif; }[]; + + result.sort((a, b) => b.score - a.score); + props.favorites = result.map(e => e.gif); + + instance.forceUpdate(); + }, [instance.state]); + + useEffect(() => { + return () => { + instance.dead = true; + }; + }, []); + + return ( + { + setQuery(""); + if (instance.props.favCopy != null) { + instance.props.favorites = instance.props.favCopy; + instance.forceUpdate(); + } + }} + query={query} + placeholder="Search Favorite Gifs" + /> + ); +} + + + +export function getTargetString(urlStr: string) { + const url = new URL(urlStr); + switch (settings.store.searchOption) { + case "url": + return url.href; + case "path": + if (url.host === "media.discordapp.net" || url.host === "tenor.com") + // /attachments/899763415290097664/1095711736461537381/attachment-1.gif -> attachment-1.gif + // /view/some-gif-hi-24248063 -> some-gif-hi-24248063 + return url.pathname.split("/").at(-1) ?? url.pathname; + return url.pathname; + case "hostandpath": + if (url.host === "media.discordapp.net" || url.host === "tenor.com") + return `${url.host} ${url.pathname.split("/").at(-1) ?? url.pathname}`; + return `${url.host} ${url.pathname}`; + + default: + return ""; + } +} + +function fuzzySearch(searchQuery: string, searchString: string) { + let searchIndex = 0; + let score = 0; + + for (let i = 0; i < searchString.length; i++) { + if (searchString[i] === searchQuery[searchIndex]) { + score++; + searchIndex++; + } else { + score--; + } + + if (searchIndex === searchQuery.length) { + return score; + } + } + + return null; +} diff --git a/src/plugins/imageZoom/README.md b/src/plugins/imageZoom/README.md new file mode 100644 index 0000000..8e3b7ef --- /dev/null +++ b/src/plugins/imageZoom/README.md @@ -0,0 +1,6 @@ +# ImageZoom + +Lets you zoom in to images and gifs. Use scroll wheel to zoom in and shift + scroll wheel to increase lens radius / size + +![Example](https://i.imgur.com/VJdo4aq.png) +![ContextMenu](https://i.imgur.com/0oaRM2s.png) diff --git a/src/plugins/imageZoom/index.tsx b/src/plugins/imageZoom/index.tsx index 71540f2..cca0db0 100644 --- a/src/plugins/imageZoom/index.tsx +++ b/src/plugins/imageZoom/index.tsx @@ -99,6 +99,15 @@ const imageContextMenuPatch: NavContextMenuPatchCallback = children => () => { ContextMenu.close(); }} /> + { + settings.store.nearestNeighbour = !settings.store.nearestNeighbour; + ContextMenu.close(); + }} + /> . -*/ - -import { sendBotMessage } from "@api/Commands"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants"; -import definePlugin from "@utils/types"; -import { Button, ButtonLooks, ButtonWrapperClasses, DraftStore, DraftType, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common"; - -interface Props { - type: { - analyticsName: string; - }; -} - -const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage); - -export function PreviewButton(chatBoxProps: Props) { - const channelId = SelectedChannelStore.getChannelId(); - const draft = useStateFromStores([DraftStore], () => getDraft(channelId)); - if (chatBoxProps.type.analyticsName !== "normal") return null; - if (!draft) return null; - - return ( - - {tooltipProps => ( - - )} - - ); - -} - -export default definePlugin({ - name: "PreviewMessage", - description: "Lets you preview your message before sending it", - authors: [Devs.Aria], - patches: [ - { - find: ".activeCommandOption", - replacement: { - match: /(.)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, - replace: "$&;try{$2||$1.unshift($self.previewIcon(arguments[0]))}catch{}", - } - }, - ], - - previewIcon: ErrorBoundary.wrap(PreviewButton, { noop: true }), -}); diff --git a/src/plugins/previewMessage/README.md b/src/plugins/previewMessage/README.md new file mode 100644 index 0000000..e0043b2 --- /dev/null +++ b/src/plugins/previewMessage/README.md @@ -0,0 +1,5 @@ +# PreviewMessage + +Lets you preview your message before sending it. + +![Example](https://i.imgur.com/etqbkzu.png) diff --git a/src/plugins/previewMessage/index.tsx b/src/plugins/previewMessage/index.tsx new file mode 100644 index 0000000..393b520 --- /dev/null +++ b/src/plugins/previewMessage/index.tsx @@ -0,0 +1,141 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { generateId, sendBotMessage } from "@api/Commands"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { Button, ButtonLooks, ButtonWrapperClasses, DraftStore, DraftType, SelectedChannelStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common"; +import { MessageAttachment } from "discord-types/general"; + +interface Props { + type: { + analyticsName: string; + isEmpty: boolean; + attachments: boolean; + }; +} + +const UploadStore = findByPropsLazy("getUploads"); + +const getDraft = (channelId: string) => DraftStore.getDraft(channelId, DraftType.ChannelMessage); + + +const getImageBox = (url: string): Promise<{ width: number, height: number; } | null> => + new Promise(res => { + const img = new Image(); + img.onload = () => + res({ width: img.width, height: img.height }); + + img.onerror = () => + res(null); + + img.src = url; + }); + + +const getAttachments = async (channelId: string) => + await Promise.all( + UploadStore.getUploads(channelId, DraftType.ChannelMessage) + .map(async (upload: any) => { + const { isImage, filename, spoiler, item: { file } } = upload; + const url = URL.createObjectURL(file); + const attachment: MessageAttachment = { + id: generateId(), + filename: spoiler ? "SPOILER_" + filename : filename, + // weird eh? if i give it the normal content type the preview doenst work + content_type: undefined, + size: await upload.getSize(), + spoiler, + // discord adds query params to the url, so we need to add a hash to prevent that + url: url + "#", + proxy_url: url + "#", + }; + + if (isImage) { + const box = await getImageBox(url); + if (!box) return attachment; + + attachment.width = box.width; + attachment.height = box.height; + } + + return attachment; + }) + ); + + +export function PreviewButton(chatBoxProps: Props) { + const { isEmpty, attachments } = chatBoxProps.type; + + const channelId = SelectedChannelStore.getChannelId(); + const draft = useStateFromStores([DraftStore], () => getDraft(channelId)); + + if (chatBoxProps.type.analyticsName !== "normal") return null; + + const hasAttachments = attachments && UploadStore.getUploads(channelId, DraftType.ChannelMessage).length > 0; + const hasContent = !isEmpty && draft?.length > 0; + + if (!hasContent && !hasAttachments) return null; + + return ( + + {tooltipProps => ( + + )} + + ); + +} + +export default definePlugin({ + name: "PreviewMessage", + description: "Lets you preview your message before sending it.", + authors: [Devs.Aria], + patches: [ + { + find: ".activeCommandOption", + replacement: { + match: /(.)\.push.{1,30}disabled:(\i),.{1,20}\},"gift"\)\)/, + replace: "$&;try{$2||$1.unshift($self.previewIcon(arguments[0]))}catch{}", + } + }, + ], + + previewIcon: ErrorBoundary.wrap(PreviewButton, { noop: true }), +}); diff --git a/src/plugins/searchReply.tsx b/src/plugins/searchReply.tsx deleted file mode 100644 index 9e53436..0000000 --- a/src/plugins/searchReply.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2023 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; -import { Devs } from "@utils/constants"; -import { LazyComponent } from "@utils/react"; -import definePlugin from "@utils/types"; -import { findByCode, findByCodeLazy } from "@webpack"; -import { ChannelStore, i18n, Menu, SelectedChannelStore } from "@webpack/common"; -import { Message } from "discord-types/general"; - -const ReplyIcon = LazyComponent(() => findByCode("M10 8.26667V4L3 11.4667L10 18.9333V14.56C15 14.56 18.5 16.2667 21 20C20 14.6667 17 9.33333 10 8.26667Z")); - -const replyFn = findByCodeLazy("showMentionToggle", "TEXTAREA_FOCUS", "shiftKey"); - -const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => () => { - // make sure the message is in the selected channel - if (SelectedChannelStore.getChannelId() !== message.channel_id) return; - - const channel = ChannelStore.getChannel(message?.channel_id); - if (!channel) return; - - // dms and group chats - const dmGroup = findGroupChildrenByChildId("pin", children); - if (dmGroup && !dmGroup.some(child => child?.props?.id === "reply")) { - const pinIndex = dmGroup.findIndex(c => c?.props.id === "pin"); - return dmGroup.splice(pinIndex + 1, 0, ( - replyFn(channel, message, e)} - /> - )); - } - - // servers - const serverGroup = findGroupChildrenByChildId("mark-unread", children); - if (serverGroup && !serverGroup.some(child => child?.props?.id === "reply")) { - return serverGroup.unshift(( - replyFn(channel, message, e)} - /> - )); - } -}; - - -export default definePlugin({ - name: "SearchReply", - description: "Adds a reply button to search results", - authors: [Devs.Aria], - - start() { - addContextMenuPatch("message", messageContextMenuPatch); - }, - - stop() { - removeContextMenuPatch("message", messageContextMenuPatch); - } -}); diff --git a/src/plugins/searchReply/README.md b/src/plugins/searchReply/README.md new file mode 100644 index 0000000..6938def --- /dev/null +++ b/src/plugins/searchReply/README.md @@ -0,0 +1,5 @@ +# SearchReply + +Adds a reply button to search results. + +![Screenshot](https://i.imgur.com/SjIEHpw.png) diff --git a/src/plugins/searchReply/index.tsx b/src/plugins/searchReply/index.tsx new file mode 100644 index 0000000..9e53436 --- /dev/null +++ b/src/plugins/searchReply/index.tsx @@ -0,0 +1,79 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { addContextMenuPatch, findGroupChildrenByChildId, NavContextMenuPatchCallback, removeContextMenuPatch } from "@api/ContextMenu"; +import { Devs } from "@utils/constants"; +import { LazyComponent } from "@utils/react"; +import definePlugin from "@utils/types"; +import { findByCode, findByCodeLazy } from "@webpack"; +import { ChannelStore, i18n, Menu, SelectedChannelStore } from "@webpack/common"; +import { Message } from "discord-types/general"; + +const ReplyIcon = LazyComponent(() => findByCode("M10 8.26667V4L3 11.4667L10 18.9333V14.56C15 14.56 18.5 16.2667 21 20C20 14.6667 17 9.33333 10 8.26667Z")); + +const replyFn = findByCodeLazy("showMentionToggle", "TEXTAREA_FOCUS", "shiftKey"); + +const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => () => { + // make sure the message is in the selected channel + if (SelectedChannelStore.getChannelId() !== message.channel_id) return; + + const channel = ChannelStore.getChannel(message?.channel_id); + if (!channel) return; + + // dms and group chats + const dmGroup = findGroupChildrenByChildId("pin", children); + if (dmGroup && !dmGroup.some(child => child?.props?.id === "reply")) { + const pinIndex = dmGroup.findIndex(c => c?.props.id === "pin"); + return dmGroup.splice(pinIndex + 1, 0, ( + replyFn(channel, message, e)} + /> + )); + } + + // servers + const serverGroup = findGroupChildrenByChildId("mark-unread", children); + if (serverGroup && !serverGroup.some(child => child?.props?.id === "reply")) { + return serverGroup.unshift(( + replyFn(channel, message, e)} + /> + )); + } +}; + + +export default definePlugin({ + name: "SearchReply", + description: "Adds a reply button to search results", + authors: [Devs.Aria], + + start() { + addContextMenuPatch("message", messageContextMenuPatch); + }, + + stop() { + removeContextMenuPatch("message", messageContextMenuPatch); + } +}); diff --git a/src/webpack/common/types/menu.d.ts b/src/webpack/common/types/menu.d.ts index 1988617..29f3ffa 100644 --- a/src/webpack/common/types/menu.d.ts +++ b/src/webpack/common/types/menu.d.ts @@ -65,8 +65,13 @@ export interface Menu { id: string; interactive?: boolean; }>; - // TODO: Type me - MenuSliderControl: RC; + MenuSliderControl: RC<{ + minValue: number, + maxValue: number, + value: number, + onChange(value: number): void, + renderValue?(value: number): string, + }>; } export interface ContextMenuApi { -- cgit