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.
+
+
+
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.
+
+
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
+
+
+
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.
+
+
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.
+
+
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