Compare commits

...
Sign in to create a new pull request.

117 commits
hive ... main

Author SHA1 Message Date
Eve
b79b102833
add the plugin
Some checks failed
Build DevBuild / Build (push) Has been cancelled
Sync to Codeberg / codeberg (push) Has been cancelled
test / test (push) Has been cancelled
2025-09-29 20:56:18 -04:00
Eve
2a33d719ee Bump Vencord version. 2025-09-29 16:53:07 -04:00
sadan4
8943c90cb0
arRPC: increase connection timeout to 5s (#3680)
Co-authored-by: V <vendicated@riseup.net>
2025-09-28 21:34:23 +00:00
V
f040133412
QuickReply/MessageClickActions: ignore non-replyable & ephemeral messages (#3692)
Co-authored-by: YashRaj <91825864+YashRajCodes@users.noreply.github.com>
2025-09-28 23:18:23 +02:00
TheRealClarity
631c763fc4
YoutubeAdblock: fix blocking in watch together activity (#3690) 2025-09-28 22:46:14 +02:00
sadan4
79c5cf5304
fix plugins for latest Discord update (#3681)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
2025-09-27 21:59:18 +02:00
Vendicated
c5a1bbd7db
GameActivityToggle: fix background colour when using nameplate 2025-09-25 15:19:45 +02:00
thororen
7c839be64f
BetterFolders: close folder if the last server is removed (#3658)
Co-authored-by: V <vendicated@riseup.net>
2025-09-24 14:49:50 +00:00
Vendicated
cb845b5224
PlatformIndicators: update indicators in real time if status changes
Closes #3516
2025-09-23 23:58:47 +02:00
Vendicated
746c824020
Fix Debug Logging toggle not working
Closes #3268
2025-09-23 22:24:58 +02:00
Vendicated
228b85a0c8
bump to v1.13.0 2025-09-22 05:18:10 +02:00
thororen
2e9d67f0b4
add Vencord badges to user settings section (#3667)
Co-authored-by: V <vendicated@riseup.net>
2025-09-22 05:15:14 +02:00
nin0
edd68fe08e
AppleMusicRichPresence: add status display type (#3669)
Co-authored-by: V <vendicated@riseup.net>
2025-09-22 03:08:08 +00:00
quarty
80872f4ab9
MessageLatency: add option to ignore own messages (#3677)
Co-authored-by: V <vendicated@riseup.net>
2025-09-22 02:44:13 +00:00
thororen
8c2dc84f3b
Settings: fix debug info layout (#3673) 2025-09-22 04:38:57 +02:00
Vendicated
3005906a28
CallTimer: fix horizontal text cutoff 2025-09-20 20:37:28 +02:00
Gabriel
56d25b03f9
UserVoiceShow: Improve tooltip & add icons for muted/deafened (#3630)
Co-authored-by: V <vendicated@riseup.net>
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
2025-09-17 03:35:01 +02:00
sadan4
fbc2dbe781
FakeNitro: fix nitro themes not working for some users (#3666) 2025-09-10 04:36:34 +02:00
thororen
9c0af5adee
Fix broken MessagePopoverAPI not working (#3661) 2025-09-09 16:28:37 +00:00
Vendicated
479d01a1b9
fix FavGifSearch regression 2025-09-09 03:57:00 +02:00
thororen
8eabb11125
ClearURLs: use rules from ClearURLs browser extension (#3657)
Co-authored-by: V <vendicated@riseup.net>
2025-09-09 01:35:01 +00:00
Nuckyz
98058f0cae
Fix mistakes 2025-09-08 22:27:41 -03:00
Nuckyz
50eb62045a
Fix FavoriteGifSearch & broken Menu component find 2025-09-08 22:18:39 -03:00
Nuckyz
4e8d22b4d5
NoPendingCount: Improve slow patch 2025-09-08 22:02:04 -03:00
sadan4
51c23ff796
FakeNitro: Add custom client theme color picker (#3534) 2025-09-08 21:51:03 -03:00
Vendicated
dc72ee3809
GameActivityToggle: fix overflow 2025-09-09 02:23:57 +02:00
thororen1234
4c7acbbbc7
fix NoPendingCount 2025-09-09 02:08:44 +02:00
thororen
0f29eab3ea
MemberCount: use circle svg instead of css hacks (#3653)
fixes some deformations
2025-09-09 02:03:12 +02:00
V
c38aac23fd
improve various types (#3663)
Co-authored-by: V <vendicated@riseup.net>
Co-authored-by: John Davis <70701251+gobuster@users.noreply.github.com>
Co-authored-by: sadan4 <117494111+sadan4@users.noreply.github.com>
2025-09-08 23:48:53 +00:00
Ryan Cao
84957b0e88
improve wordsFromCamel correctness (#3621)
Co-authored-by: V <vendicated@riseup.net>
2025-09-09 00:49:24 +02:00
ww
a4e1d026ea
VolumeBooster: make multiplier option more flexible (#3656) 2025-09-08 01:30:08 +00:00
Vendicated
b225f2ec6c
SpotifyControls: add copy song/artist/album name options 2025-09-08 02:55:02 +02:00
nin0
efecbae75b
LastFMRPC: add setting to show artist/song name in member list (#3629) 2025-09-08 00:14:38 +00:00
thororen
1cfc3fb8f8
fix OnePingPerDM (#3648)
also removes a now obsolete patch from FakeNitro
2025-09-06 18:27:29 +02:00
u32
26074b7f18
MessageLatency: fix bot check (#3523) 2025-09-05 02:04:08 +00:00
Vendicated
e857f6806f
ShowMeYourName: respect streamer mode 2025-09-05 03:55:38 +02:00
Gleb P
b6e96a4d3b
MemberCount: also show members in voice (#2937)
Co-authored-by: Vendicated <vendicated@riseup.net>
2025-09-05 03:37:13 +02:00
sadan4
1d00ba4161
fix patches for Experiments and Vencord Toolbox (#3647) 2025-09-04 21:59:30 +02:00
union
77b016de36
ReverseImageSearch: add Bing (#2793) 2025-09-04 02:46:02 +02:00
union
b7f19bbe37
NoReplyMention: add role whitelist / blacklist (#2794)
Co-authored-by: V <vendicated@riseup.net>
2025-09-04 02:41:06 +02:00
Vendicated
9700ec9cd2
fix minor bugs 2025-09-03 04:37:38 +02:00
Vendicated
65c85a5222
CallTimer: fix overflow when using aligned chat input
Co-Authored-By: sadan4 <117494111+sadan4@users.noreply.github.com>
Co-Authored-By: God
2025-09-03 04:26:33 +02:00
sadan4
8789973bf5
WhoReacted: remove ugly more users tooltip (#3640)
Co-authored-by: Vendicated <vendicated@riseup.net>
2025-09-03 03:41:46 +02:00
ayuxia
4ff3614dc0
ShowMeYourName: support friend nicknames (#3639)
Co-authored-by: V <vendicated@riseup.net>
2025-09-03 03:19:12 +02:00
thororen
5c69d340d9
Fix MutualGroupDMs & Decor broken patches (#3644) 2025-09-02 23:44:25 +00:00
Etorix
75a2506c51
ViewRaw: Adjust icon size to match other icons (#3605) 2025-09-02 16:42:17 +00:00
jamesbt365
9b0ae0fd90
Translate: support automod & forwarded messages (#3367)
Co-authored-by: V <vendicated@riseup.net>
2025-09-02 04:20:56 +02:00
sadan4
f0f75aa918
AlwaysAnimate: Add nameplates support (#3641) 2025-09-01 23:59:15 +00:00
Nuckyz
8ebfd9a190
NoTypingAnimation: Fix not working due to broken patches 2025-09-01 20:53:43 -03:00
Vendicated
17b90beee1
add context menu options to vencord badges 2025-08-31 05:18:47 +02:00
Nuckyz
8807564053
ConsoleJanitor: Fix outdated settings margin 2025-08-29 16:23:23 -03:00
sadan4
aca30bcb9a
ShowHiddenChannels: Fix broken patch (#3635) 2025-08-29 19:02:45 +00:00
thororen
67aff64fed
MutualGroupDMs: Fix broken patch (#3634) 2025-08-29 16:01:27 -03:00
Vendicated
c5888c25f7
bump to v1.12.13 2025-08-29 02:52:44 +02:00
thororen
19c1eaed18 fix failing to find the Select component (#3631) 2025-08-28 04:55:03 +02:00
Nuckyz
5dee746986 Fix IgnoreActivities 2025-08-28 04:55:03 +02:00
Vendicated
76a60e07e9
fix SuperReactionTweaks 2025-08-23 02:16:32 +02:00
Vendicated
abe910d80d
fix not being able to dismiss Vencord Notices 2025-08-23 02:02:02 +02:00
Vendicated
4a35cf1769
Toolbox: fix & move to the titlebar 2025-08-23 01:57:27 +02:00
fawn
643656d798
new plugin ImageFilename: displays image filename tooltips on hover (#3617)
Co-authored-by: Vendicated <vendicated@riseup.net>
2025-08-19 16:41:02 +02:00
Vendicated
0bb2ed6b72
bump to v1.12.12 2025-08-18 18:58:40 +02:00
Nuckyz
330c3cead7
Fix plugins broken by latest Discord update (#3609) 2025-08-17 17:33:23 -03:00
Vendicated
93294673de
bump to v1.12.11 2025-08-14 21:48:28 +02:00
sadan4
204f916b2a
fix Settings UI and various plugins (#3608)
Co-authored-by: Vendicated <vendicated@riseup.net>
2025-08-14 21:42:58 +02:00
Vendicated
f356f647ff
bump to v1.12.10 2025-08-13 12:58:29 +02:00
Vendicated
aad88fe9cd
fix plugins sending messages 2025-08-13 12:55:24 +02:00
sadan4
72329f901c
AccountPanelServerProfile: fix right click menu (#3600)
Co-authored-by: Vendicated <vendicated@riseup.net>
2025-08-12 03:28:19 +02:00
Vendicated
27b2e97e3f
ReviewDB: fix input 2025-08-12 03:00:47 +02:00
Nuckyz
d9e2732a8d
Fix MessageEventsAPI broken patch 2025-08-11 15:35:29 -03:00
Vendicated
0c89314d49
PermissionFreeWill: don't break permission toggles 2025-08-07 23:38:37 +02:00
byeoon
4403aee3c1
New plugin CopyStickerLinks: adds Copy/Open Link option to stickers (#3191)
Co-authored-by: V <vendicated@riseup.net>
2025-08-07 22:24:13 +02:00
sadan4
7e028267f1
SpotifyShareCommands: correctly handle local tracks (#3592) 2025-08-07 21:04:59 +02:00
Nuckyz
c7e799e935
ShowHiddenChannels: Fix incorrect allowed users and roles component 2025-08-07 09:29:37 -03:00
Vendicated
98efe13b97
ShowHiddenThings: fix crash when viewing Mod View 2025-08-07 04:24:34 +02:00
Vendicated
164fd43cc4
Fix IgnoreActivities 2025-08-06 22:01:24 -03:00
Nuckyz
fe2ed0776f
ShowHiddenChannels: Fix showing allowed roles 2025-08-06 22:01:24 -03:00
sadan4
74d78d89ed
fix TypingTweaks (#3586)
Co-authored-by: V <vendicated@riseup.net>
2025-08-07 02:11:38 +02:00
sadan4
6a66b7f54f
fix plugins broken by Discord update (#3583)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
Co-authored-by: V <vendicated@riseup.net>
2025-08-06 22:16:07 +02:00
Zordan
36c15d619e
HideAttachments: correctly support forwarded Messages (#3587)
Co-authored-by: V <vendicated@riseup.net>
2025-08-06 16:26:00 +02:00
V
a2253cb4ae
remove old discord ui workarounds & legacy code (#3585)
Co-authored-by: sadan <117494111+sadan4@users.noreply.github.com>
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
2025-08-05 23:57:51 +02:00
V
6380111f32
Notification Log: fix lag if there are too many entries (#3584)
Use Discord's lazy list implementation for only rendering what's on screen
2025-08-05 19:20:28 +02:00
Vendicated
1ebd412392
BetterUploadButton: don't affect other chat buttons 2025-08-03 17:04:50 +02:00
Vendicated
a02d1afdf0
bump to v1.12.8 2025-08-03 16:26:08 +02:00
Vendicated
38e46f89cf
BetterUploadButton: fix right click action not working 2025-08-03 16:24:00 +02:00
sadan4
22d3fb10e9
fix plugins broken by latest Discord update (#3574)
Co-authored-by: V <vendicated@riseup.net>
2025-08-03 13:05:26 +00:00
Vendicated
fa672a347d
BetterSessions: fix ui 2025-08-01 01:51:38 +02:00
sadan4
cb36cf5706
fix Plugins broken by recent Discord changes (#3569)
Co-authored-by: Vendicated <vendicated@riseup.net>
2025-07-31 22:31:33 +02:00
Nuckyz
29a2bcbf07
Fix BetterUploadButton not working 2025-07-26 11:31:30 -03:00
Vendicated
03fe7d15cf
bump to v1.12.7 2025-07-22 22:45:28 +02:00
Vendicated
6fb685b959
Fix Plugin settings page
Co-Authored-By: sadan <117494111+sadan4@users.noreply.github.com>
2025-07-22 22:43:31 +02:00
sadan4
8b36013264
fix GreetStickerPicker (#3561) 2025-07-22 22:35:35 +02:00
Vendicated
50e2ad776b
Plugin Settings: improve headings 2025-07-21 12:32:20 +02:00
Vendicated
4ab084c0ac
make ShowMeYourName patch safer 2025-07-21 12:13:13 +02:00
Vendicated
113a1f4b86
fix ShowMeYourName 2025-07-21 12:09:36 +02:00
Nuckyz
25cd6b7069
Remove unused and non unique webpack common style classes find 2025-07-17 22:06:22 -03:00
Nuckyz
f6f0624e52
Fix broken Decor style classes find 2025-07-17 22:05:42 -03:00
Nuckyz
cdda1224ff
UserVoiceShow: Improve icon in User Profile Modal V2 2025-07-16 20:23:06 -03:00
Nuckyz
d0869c41cd
Settings: Improve layout of a setting section and error 2025-07-16 18:15:52 -03:00
Nuckyz
828358bd2e
IrcColors: Fix chat colors broken patch 2025-07-15 15:52:48 -03:00
V
3f51ee1b2a
refactor Settings UI (#3545)
Much improved file structure and cleaner code. Also gets rid of temporary settings & saving and instead applies all changes immediately.

Besides that, this change only changes code and doesn't change the ui
2025-07-15 15:57:24 +02:00
Nuckyz
a33e81d1cb
Bump to v1.12.6 2025-07-14 21:05:04 -03:00
Nuckyz
f3874d0a26
Fix Plugin Settings broken webpack find 2025-07-14 21:02:44 -03:00
Nuckyz
ad810df978
Attempt to make OverrideForumDefaults not always slow 2025-07-14 17:09:21 -03:00
Nuckyz
1a98d54e3a
Fix Experiments embed patches 2025-07-14 17:07:43 -03:00
Nuckyz
5dd6722528
Fix MessagePopoverAPI incorrectly hiding the emoji button 2025-07-14 17:07:22 -03:00
sadan4
6787e98003
FakeProfileThemes: fix error when own profile is not loaded (#3514)
Co-authored-by: V <vendicated@riseup.net>
2025-07-10 22:38:27 +02:00
sadan4
18f083b7e6
fix ImageZoom & FakeProfileThemes (#3546)
Co-authored-by: Vendicated <vendicated@riseup.net>
2025-07-10 19:56:12 +00:00
Vendicated
19f4d7cdac
fix various plugins still using outdated Discord classes 2025-07-10 01:37:32 +02:00
Vendicated
8e446e44ab
Online Themes: fix & improve ui 2025-07-10 01:37:13 +02:00
Cookie
a17803c1c4
Settings: remove outdated style (#3490) 2025-07-09 22:42:38 +00:00
V
dc9064326b
refactor discord types into separate npm package (#3520)
Co-authored-by: V <vendicated@riseup.net>
Co-authored-by: sadan <117494111+sadan4@users.noreply.github.com>
Co-authored-by: ezzud <contact@ezzud.fr>
2025-07-10 00:38:39 +02:00
nyx
c55833d95f
ShowMeYourName: fix gradient role compatibility (#3495)
Co-authored-by: V <vendicated@riseup.net>
2025-07-08 01:05:56 +02:00
Amia
44f7ccafcb
MutualGroupDMs: fix member count label (#3541) 2025-07-08 01:04:14 +02:00
Etorix
643122e323
Experiments: fix edge case in experiment rollout detection (#3497)
Co-authored-by: V <vendicated@riseup.net>
2025-07-04 16:09:10 +02:00
Amia
310d8e6140
Fix ImplicitRelationships & RelationshipNotifier (#3539) 2025-07-04 15:04:12 +02:00
Vendicated
1142cab05c
Merge remote-tracking branch 'origin/main' into dev 2025-07-04 14:59:44 +02:00
sadan4
c653e36137
fix Expression Cloner, ServerInfo & SeeSummaries plugins (#3536) 2025-07-04 02:14:59 +02:00
326 changed files with 6617 additions and 4377 deletions

2
.gitignore vendored
View file

@ -22,4 +22,4 @@ lerna-debug.log*
src/userplugins
ExtensionCache/
settings/
/settings

View file

@ -13,11 +13,14 @@
"typescript.format.semicolons": "insert",
"typescript.preferences.quoteStyle": "double",
"javascript.preferences.quoteStyle": "double",
"gitlens.remotes": [
{
"domain": "codeberg.org",
"type": "Gitea"
}
]
],
"css.format.spaceAroundSelectorSeparator": true,
"[css]": {
"editor.defaultFormatter": "vscode.css-language-features"
}
}

View file

@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
"version": "1.12.5",
"version": "1.13.0",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
@ -53,8 +53,8 @@
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/yazl": "^2.4.5",
"@vencord/discord-types": "link:packages/discord-types",
"diff": "^7.0.0",
"discord-types": "^1.3.26",
"esbuild": "^0.25.1",
"eslint": "9.20.1",
"eslint-import-resolver-alias": "^1.1.2",

View file

@ -0,0 +1 @@
Hint: https://docs.discord.food is an incredible resource and allows you to copy paste complete enums and interfaces

View file

@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View file

@ -0,0 +1,42 @@
# Discord Types
This package provides TypeScript types for the Webpack modules of Discord's web app.
While it was primarily created for Vencord, other client mods could also benefit from this, so it is published as a standalone package!
## Installation
```bash
npm install -D @vencord/discord-types
yarn add -D @vencord/discord-types
pnpm add -D @vencord/discord-types
```
## Example Usage
```ts
import type { UserStore } from "@vencord/discord-types";
const userStore: UserStore = findStore("UserStore"); // findStore is up to you to implement, this library only provides types and no runtime code
```
## Enums
This library also exports some const enums that you can use from Typescript code:
```ts
import { ApplicationCommandType } from "@vencord/discord-types/enums";
console.log(ApplicationCommandType.CHAT_INPUT); // 1
```
### License
This package is licensed under the [LGPL-3.0](./LICENSE) (or later) license.
A very short summary of the license is that you can use this package as a library in both open source and closed source projects,
similar to an MIT-licensed project.
However, if you modify the code of this package, you must release source code of your modified version under the same license.
### Credit
This package was inspired by Swishilicous' [discord-types](https://www.npmjs.com/package/discord-types) package.

View file

@ -0,0 +1,30 @@
export const enum ActivityType {
PLAYING = 0,
STREAMING = 1,
LISTENING = 2,
WATCHING = 3,
CUSTOM_STATUS = 4,
COMPETING = 5,
HANG_STATUS = 6
}
export const enum ActivityFlags {
INSTANCE = 1 << 0,
JOIN = 1 << 1,
/** @deprecated */
SPECTATE = 1 << 2,
/** @deprecated */
JOIN_REQUEST = 1 << 3,
SYNC = 1 << 4,
PLAY = 1 << 5,
PARTY_PRIVACY_FRIENDS = 1 << 6,
PARTY_PRIVACY_VOICE_CHANNEL = 1 << 7,
EMBEDDED = 1 << 8,
CONTEXTLESS = 1 << 9
}
export const enum ActivityStatusDisplayType {
NAME = 0,
STATE = 1,
DETAILS = 2
}

View file

@ -0,0 +1,15 @@
export const enum ChannelType {
GUILD_TEXT = 0,
DM = 1,
GUILD_VOICE = 2,
GROUP_DM = 3,
GUILD_CATEGORY = 4,
GUILD_ANNOUNCEMENT = 5,
ANNOUNCEMENT_THREAD = 10,
PUBLIC_THREAD = 11,
PRIVATE_THREAD = 12,
GUILD_STAGE_VOICE = 13,
GUILD_DIRECTORY = 14,
GUILD_FORUM = 15,
GUILD_MEDIA = 16
}

View file

@ -0,0 +1,32 @@
export const enum ApplicationCommandOptionType {
SUB_COMMAND = 1,
SUB_COMMAND_GROUP = 2,
STRING = 3,
INTEGER = 4,
BOOLEAN = 5,
USER = 6,
CHANNEL = 7,
ROLE = 8,
MENTIONABLE = 9,
NUMBER = 10,
ATTACHMENT = 11,
}
export const enum ApplicationCommandInputType {
BUILT_IN = 0,
BUILT_IN_TEXT = 1,
BUILT_IN_INTEGRATION = 2,
BOT = 3,
PLACEHOLDER = 4,
}
export const enum ApplicationCommandType {
CHAT_INPUT = 1,
USER = 2,
MESSAGE = 3,
}
export const enum ApplicationIntegrationType {
GUILD_INSTALL = 0,
USER_INSTALL = 1
}

View file

@ -0,0 +1,5 @@
export * from "./activity";
export * from "./channel";
export * from "./commands";
export * from "./messages";
export * from "./misc";

View file

@ -0,0 +1,596 @@
export const enum StickerType {
/** an official sticker in a pack */
STANDARD = 1,
/** a sticker uploaded to a guild for the guild's members */
GUILD = 2
}
export const enum StickerFormatType {
PNG = 1,
APNG = 2,
LOTTIE = 3,
GIF = 4
}
export const enum MessageType {
/**
* A default message (see below)
*
* Value: 0
* Name: DEFAULT
* Rendered Content: "{content}"
* Deletable: true
*/
DEFAULT = 0,
/**
* A message sent when a user is added to a group DM or thread
*
* Value: 1
* Name: RECIPIENT_ADD
* Rendered Content: "{author} added {mentions [0] } to the {group/thread}."
* Deletable: false
*/
RECIPIENT_ADD = 1,
/**
* A message sent when a user is removed from a group DM or thread
*
* Value: 2
* Name: RECIPIENT_REMOVE
* Rendered Content: "{author} removed {mentions [0] } from the {group/thread}."
* Deletable: false
*/
RECIPIENT_REMOVE = 2,
/**
* A message sent when a user creates a call in a private channel
*
* Value: 3
* Name: CALL
* Rendered Content: participated ? "{author} started a call{ended ? " that lasted {duration}" : " Join the call"}." : "You missed a call from {author} that lasted {duration}."
* Deletable: false
*/
CALL = 3,
/**
* A message sent when a group DM or thread's name is changed
*
* Value: 4
* Name: CHANNEL_NAME_CHANGE
* Rendered Content: "{author} changed the {is_forum ? "post title" : "channel name"}: {content} "
* Deletable: false
*/
CHANNEL_NAME_CHANGE = 4,
/**
* A message sent when a group DM's icon is changed
*
* Value: 5
* Name: CHANNEL_ICON_CHANGE
* Rendered Content: "{author} changed the channel icon."
* Deletable: false
*/
CHANNEL_ICON_CHANGE = 5,
/**
* A message sent when a message is pinned in a channel
*
* Value: 6
* Name: CHANNEL_PINNED_MESSAGE
* Rendered Content: "{author} pinned a message to this channel."
* Deletable: true
*/
CHANNEL_PINNED_MESSAGE = 6,
/**
* A message sent when a user joins a guild
*
* Value: 7
* Name: USER_JOIN
* Rendered Content: See user join message type , obtained via the formula timestamp_ms % 13
* Deletable: true
*/
USER_JOIN = 7,
/**
* A message sent when a user subscribes to (boosts) a guild
*
* Value: 8
* Name: PREMIUM_GUILD_SUBSCRIPTION
* Rendered Content: "{author} just boosted the server{content ? " {content} times"}!"
* Deletable: true
*/
PREMIUM_GUILD_SUBSCRIPTION = 8,
/**
* A message sent when a user subscribes to (boosts) a guild to tier 1
*
* Value: 9
* Name: PREMIUM_GUILD_SUBSCRIPTION_TIER_1
* Rendered Content: "{author} just boosted the server{content ? " {content} times"}! {guild} has achieved Level 1! "
* Deletable: true
*/
PREMIUM_GUILD_SUBSCRIPTION_TIER_1 = 9,
/**
* A message sent when a user subscribes to (boosts) a guild to tier 2
*
* Value: 10
* Name: PREMIUM_GUILD_SUBSCRIPTION_TIER_2
* Rendered Content: "{author} just boosted the server{content ? " {content} times"}! {guild} has achieved Level 2! "
* Deletable: true
*/
PREMIUM_GUILD_SUBSCRIPTION_TIER_2 = 10,
/**
* A message sent when a user subscribes to (boosts) a guild to tier 3
*
* Value: 11
* Name: PREMIUM_GUILD_SUBSCRIPTION_TIER_3
* Rendered Content: "{author} just boosted the server{content ? " {content} times"}! {guild} has achieved Level 3! "
* Deletable: true
*/
PREMIUM_GUILD_SUBSCRIPTION_TIER_3 = 11,
/**
* A message sent when a news channel is followed
*
* Value: 12
* Name: CHANNEL_FOLLOW_ADD
* Rendered Content: "{author} has added {content} to this channel. Its most important updates will show up here."
* Deletable: true
*/
CHANNEL_FOLLOW_ADD = 12,
/**
* A message sent when a guild is disqualified from discovery
*
* Value: 14
* Name: GUILD_DISCOVERY_DISQUALIFIED
* Rendered Content: "This server has been removed from Server Discovery because it no longer passes all the requirements. Check Server Settings for more details."
* Deletable: true
*/
GUILD_DISCOVERY_DISQUALIFIED = 14,
/**
* A message sent when a guild requalifies for discovery
*
* Value: 15
* Name: GUILD_DISCOVERY_REQUALIFIED
* Rendered Content: "This server is eligible for Server Discovery again and has been automatically relisted!"
* Deletable: true
*/
GUILD_DISCOVERY_REQUALIFIED = 15,
/**
* A message sent when a guild has failed discovery requirements for a week
*
* Value: 16
* Name: GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING
* Rendered Content: "This server has failed Discovery activity requirements for 1 week. If this server fails for 4 weeks in a row, it will be automatically removed from Discovery."
* Deletable: true
*/
GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING = 16,
/**
* A message sent when a guild has failed discovery requirements for 3 weeks
*
* Value: 17
* Name: GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING
* Rendered Content: "This server has failed Discovery activity requirements for 3 weeks in a row. If this server fails for 1 more week, it will be removed from Discovery."
* Deletable: true
*/
GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING = 17,
/**
* A message sent when a thread is created
*
* Value: 18
* Name: THREAD_CREATED
* Rendered Content: "{author} started a thread: {content} . See all threads."
* Deletable: true
*/
THREAD_CREATED = 18,
/**
* A message sent when a user replies to a message
*
* Value: 19
* Name: REPLY
* Rendered Content: "{content}"
* Deletable: true
*/
REPLY = 19,
/**
* A message sent when a user uses a slash command
*
* Value: 20
* Name: CHAT_INPUT_COMMAND
* Rendered Content: "{content}"
* Deletable: true
*/
CHAT_INPUT_COMMAND = 20,
/**
* A message sent when a thread starter message is added to a thread
*
* Value: 21
* Name: THREAD_STARTER_MESSAGE
* Rendered Content: "{referenced_message?.content}" ?? "Sorry, we couldn't load the first message in this thread"
* Deletable: false
*/
THREAD_STARTER_MESSAGE = 21,
/**
* A message sent to remind users to invite friends to a guild
*
* Value: 22
* Name: GUILD_INVITE_REMINDER
* Rendered Content: "Wondering who to invite?\nStart by inviting anyone who can help you build the server!"
* Deletable: true
*/
GUILD_INVITE_REMINDER = 22,
/**
* A message sent when a user uses a context menu command
*
* Value: 23
* Name: CONTEXT_MENU_COMMAND
* Rendered Content: "{content}"
* Deletable: true
*/
CONTEXT_MENU_COMMAND = 23,
/**
* A message sent when auto moderation takes an action
*
* Value: 24
* Name: AUTO_MODERATION_ACTION
* Rendered Content: Special embed rendered from embeds[0]
* Deletable: true 1
*/
AUTO_MODERATION_ACTION = 24,
/**
* A message sent when a user purchases or renews a role subscription
*
* Value: 25
* Name: ROLE_SUBSCRIPTION_PURCHASE
* Rendered Content: "{author} {is_renewal ? "renewed" : "joined"} {role_subscription.tier_name} and has been a subscriber of {guild} for {role_subscription.total_months_subscribed} month(?s)!"
* Deletable: true
*/
ROLE_SUBSCRIPTION_PURCHASE = 25,
/**
* A message sent when a user is upsold to a premium interaction
*
* Value: 26
* Name: INTERACTION_PREMIUM_UPSELL
* Rendered Content: "{content}"
* Deletable: true
*/
INTERACTION_PREMIUM_UPSELL = 26,
/**
* A message sent when a stage channel starts
*
* Value: 27
* Name: STAGE_START
* Rendered Content: "{author} started {content} "
* Deletable: true
*/
STAGE_START = 27,
/**
* A message sent when a stage channel ends
*
* Value: 28
* Name: STAGE_END
* Rendered Content: "{author} ended {content} "
* Deletable: true
*/
STAGE_END = 28,
/**
* A message sent when a user starts speaking in a stage channel
*
* Value: 29
* Name: STAGE_SPEAKER
* Rendered Content: "{author} is now a speaker."
* Deletable: true
*/
STAGE_SPEAKER = 29,
/**
* A message sent when a user raises their hand in a stage channel
*
* Value: 30
* Name: STAGE_RAISE_HAND
* Rendered Content: "{author} requested to speak."
* Deletable: true
*/
STAGE_RAISE_HAND = 30,
/**
* A message sent when a stage channel's topic is changed
*
* Value: 31
* Name: STAGE_TOPIC
* Rendered Content: "{author} changed the Stage topic: {content} "
* Deletable: true
*/
STAGE_TOPIC = 31,
/**
* A message sent when a user purchases an application premium subscription
*
* Value: 32
* Name: GUILD_APPLICATION_PREMIUM_SUBSCRIPTION
* Rendered Content: "{author} upgraded {application ?? "a deleted application"} to premium for this server!"
* Deletable: true
*/
GUILD_APPLICATION_PREMIUM_SUBSCRIPTION = 32,
/**
* A message sent when a user gifts a premium (Nitro) referral
*
* Value: 35
* Name: PREMIUM_REFERRAL
* Rendered Content: "{content}"
* Deletable: true
*/
PREMIUM_REFERRAL = 35,
/**
* A message sent when a user enabled lockdown for the guild
*
* Value: 36
* Name: GUILD_INCIDENT_ALERT_MODE_ENABLED
* Rendered Content: "{author} enabled security actions until {content}."
* Deletable: true
*/
GUILD_INCIDENT_ALERT_MODE_ENABLED = 36,
/**
* A message sent when a user disables lockdown for the guild
*
* Value: 37
* Name: GUILD_INCIDENT_ALERT_MODE_DISABLED
* Rendered Content: "{author} disabled security actions."
* Deletable: true
*/
GUILD_INCIDENT_ALERT_MODE_DISABLED = 37,
/**
* A message sent when a user reports a raid for the guild
*
* Value: 38
* Name: GUILD_INCIDENT_REPORT_RAID
* Rendered Content: "{author} reported a raid in {guild}."
* Deletable: true
*/
GUILD_INCIDENT_REPORT_RAID = 38,
/**
* A message sent when a user reports a false alarm for the guild
*
* Value: 39
* Name: GUILD_INCIDENT_REPORT_FALSE_ALARM
* Rendered Content: "{author} reported a false alarm in {guild}."
* Deletable: true
*/
GUILD_INCIDENT_REPORT_FALSE_ALARM = 39,
/**
* A message sent when no one sends a message in the current channel for 1 hour
*
* Value: 40
* Name: GUILD_DEADCHAT_REVIVE_PROMPT
* Rendered Content: "{content}"
* Deletable: true
*/
GUILD_DEADCHAT_REVIVE_PROMPT = 40,
/**
* A message sent when a user buys another user a gift
*
* Value: 41
* Name: CUSTOM_GIFT
* Rendered Content: Special embed rendered from embeds[0].url and gift_info
* Deletable: true
*/
CUSTOM_GIFT = 41,
/**
* Value: 42
* Name: GUILD_GAMING_STATS_PROMPT
* Rendered Content: "{content}"
* Deletable: true
*/
GUILD_GAMING_STATS_PROMPT = 42,
/**
* A message sent when a user purchases a guild product
*
* Value: 44
* Name: PURCHASE_NOTIFICATION
* Rendered Content: "{author} has purchased {purchase_notification.guild_product_purchase.product_name}!"
* Deletable: true
*/
PURCHASE_NOTIFICATION = 44,
/**
* A message sent when a poll is finalized
*
* Value: 46
* Name: POLL_RESULT
* Rendered Content: Special embed rendered from embeds[0]
* Deletable: true
*/
POLL_RESULT = 46,
/**
* A message sent by the Discord Updates account when a new changelog is posted
*
* Value: 47
* Name: CHANGELOG
* Rendered Content: "{content}"
* Deletable: true
*/
CHANGELOG = 47,
/**
* A message sent when a Nitro promotion is triggered
*
* Value: 48
* Name: NITRO_NOTIFICATION
* Rendered Content: Special embed rendered from content
* Deletable: true
*/
NITRO_NOTIFICATION = 48,
/**
* A message sent when a voice channel is linked to a lobby
*
* Value: 49
* Name: CHANNEL_LINKED_TO_LOBBY
* Rendered Content: "{content}"
* Deletable: true
*/
CHANNEL_LINKED_TO_LOBBY = 49,
/**
* A local-only ephemeral message sent when a user is prompted to gift Nitro to a friend on their friendship anniversary
*
* Value: 50
* Name: GIFTING_PROMPT
* Rendered Content: Special embed
* Deletable: true
*/
GIFTING_PROMPT = 50,
/**
* A local-only message sent when a user receives an in-game message NUX
*
* Value: 51
* Name: IN_GAME_MESSAGE_NUX
* Rendered Content: "{author} messaged you from {application.name}. In-game chat may not include rich messaging features such as images, polls, or apps. Learn More "
* Deletable: true
*/
IN_GAME_MESSAGE_NUX = 51,
/**
* A message sent when a user accepts a guild join request
*
* Value: 52
* Name: GUILD_JOIN_REQUEST_ACCEPT_NOTIFICATION 2
* Rendered Content: "{join_request.user}'s application to {content} was approved! Welcome!"
* Deletable: true
*/
GUILD_JOIN_REQUEST_ACCEPT_NOTIFICATION = 52,
/**
* A message sent when a user rejects a guild join request
*
* Value: 53
* Name: GUILD_JOIN_REQUEST_REJECT_NOTIFICATION 2
* Rendered Content: "{join_request.user}'s application to {content} was rejected."
* Deletable: true
*/
GUILD_JOIN_REQUEST_REJECT_NOTIFICATION = 53,
/**
* A message sent when a user withdraws a guild join request
*
* Value: 54
* Name: GUILD_JOIN_REQUEST_WITHDRAWN_NOTIFICATION 2
* Rendered Content: "{join_request.user}'s application to {content} has been withdrawn."
* Deletable: true
*/
GUILD_JOIN_REQUEST_WITHDRAWN_NOTIFICATION = 54,
/**
* A message sent when a user upgrades to HD streaming
*
* Value: 55
* Name: HD_STREAMING_UPGRADED
* Rendered Content: "{author} activated HD Splash Potion "
* Deletable: true
*/
HD_STREAMING_UPGRADED = 55,
/**
* A message sent when a user resolves a moderation report by deleting the offending message
*
* Value: 58
* Name: REPORT_TO_MOD_DELETED_MESSAGE
* Rendered Content: "{author} deleted the message"
* Deletable: true
*/
REPORT_TO_MOD_DELETED_MESSAGE = 58,
/**
* A message sent when a user resolves a moderation report by timing out the offending user
*
* Value: 59
* Name: REPORT_TO_MOD_TIMEOUT_USER
* Rendered Content: "{author} timed out {mentions [0] }"
* Deletable: true
*/
REPORT_TO_MOD_TIMEOUT_USER = 59,
/**
* A message sent when a user resolves a moderation report by kicking the offending user
*
* Value: 60
* Name: REPORT_TO_MOD_KICK_USER
* Rendered Content: "{author} kicked {mentions [0] }"
* Deletable: true
*/
REPORT_TO_MOD_KICK_USER = 60,
/**
* A message sent when a user resolves a moderation report by banning the offending user
*
* Value: 61
* Name: REPORT_TO_MOD_BAN_USER
* Rendered Content: "{author} banned {mentions [0] }"
* Deletable: true
*/
REPORT_TO_MOD_BAN_USER = 61,
/**
* A message sent when a user resolves a moderation report
*
* Value: 62
* Name: REPORT_TO_MOD_CLOSED_REPORT
* Rendered Content: "{author} resolved this flag"
* Deletable: true
*/
REPORT_TO_MOD_CLOSED_REPORT = 62,
/**
* A message sent when a user adds a new emoji to a guild
*
* Value: 63
* Name: EMOJI_ADDED
* Rendered Content: "{author} added a new emoji, {content} :{emoji.name}: "
* Deletable: true
*/
EMOJI_ADDED = 63,
}
export const enum MessageFlags {
/**
* Message has been published to subscribed channels (via Channel Following)
*
* Value: 1 << 0
*/
CROSSPOSTED = 1 << 0,
/**
* Message originated from a message in another channel (via Channel Following)
*/
IS_CROSSPOST = 1 << 1,
/**
* Embeds will not be included when serializing this message
*/
SUPPRESS_EMBEDS = 1 << 2,
/**
* Source message for this crosspost has been deleted (via Channel Following)
*/
SOURCE_MESSAGE_DELETED = 1 << 3,
/**
* Message came from the urgent message system
*/
URGENT = 1 << 4,
/**
* Message has an associated thread, with the same ID as the message
*/
HAS_THREAD = 1 << 5,
/**
* Message is only visible to the user who invoked the interaction
*/
EPHEMERAL = 1 << 6,
/**
* Message is an interaction response and the bot is "thinking"
*/
LOADING = 1 << 7,
/**
* Some roles were not mentioned and added to the thread
*/
FAILED_TO_MENTION_SOME_ROLES_IN_THREAD = 1 << 8,
/**
* Message is hidden from the guild's feed
*/
GUILD_FEED_HIDDEN = 1 << 9,
/**
* Message contains a link that impersonates Discord
*/
SHOULD_SHOW_LINK_NOT_DISCORD_WARNING = 1 << 10,
/**
* Message will not trigger push and desktop notifications
*/
SUPPRESS_NOTIFICATIONS = 1 << 12,
/**
* Message's audio attachment is rendered as a voice message
*/
IS_VOICE_MESSAGE = 1 << 13,
/**
* Message has a forwarded message snapshot attached
*/
HAS_SNAPSHOT = 1 << 14,
/**
* Message contains components from version 2 of the UI kit
*/
IS_COMPONENTS_V2 = 1 << 15,
/**
* Message was triggered by the social layer integration
*/
SENT_BY_SOCIAL_LAYER_INTEGRATION = 1 << 16,
}

View file

@ -0,0 +1,4 @@
export const enum CloudUploadPlatform {
REACT_NATIVE = 0,
WEB = 1,
}

View file

@ -0,0 +1,19 @@
{
"name": "@vencord/discord-types",
"author": "Vencord Contributors",
"description": "Typescript definitions for the webpack modules of the Discord Web app",
"version": "1.0.0",
"license": "LGPL-3.0-or-later",
"types": "src/index.d.ts",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/Vendicated/Vencord.git",
"directory": "packages/discord-types"
},
"dependencies": {
"@types/react": "^19.0.10",
"moment": "^2.22.2",
"type-fest": "^4.41.0"
}
}

16
packages/discord-types/src/classes.d.ts vendored Normal file
View file

@ -0,0 +1,16 @@
export interface ButtonWrapperClasses {
hoverScale: string;
buttonWrapper: string;
button: string;
iconMask: string;
buttonContent: string;
icon: string;
pulseIcon: string;
pulseButton: string;
notificationDot: string;
sparkleContainer: string;
sparkleStar: string;
sparklePlus: string;
sparkle: string;
active: string;
}

View file

@ -0,0 +1,36 @@
import { ActivityFlags, ActivityStatusDisplayType, ActivityType } from "../../enums";
export interface ActivityAssets {
large_image?: string;
large_text?: string;
small_image?: string;
small_text?: string;
}
export interface ActivityButton {
label: string;
url: string;
}
export interface Activity {
name: string;
application_id: string;
type: ActivityType;
state?: string;
state_url?: string;
details?: string;
details_url?: string;
url?: string;
flags: ActivityFlags;
status_display_type?: ActivityStatusDisplayType;
timestamps?: {
start?: number;
end?: number;
};
assets?: ActivityAssets;
buttons?: string[];
metadata?: {
button_urls?: Array<string>;
};
}

View file

@ -0,0 +1,23 @@
import { User } from "./User";
export interface Application {
id: string;
name: string;
description?: string | null;
type: number | null;
icon: string | null | undefined;
is_discoverable: boolean;
is_monetized: boolean;
is_verified: boolean;
bot?: User;
deeplink_uri?: string;
flags?: number;
privacy_policy_url?: string;
terms_of_service_url?: string;
install_params?: ApplicationInstallParams;
}
export interface ApplicationInstallParams {
permissions: string | null;
scopes: string[];
}

View file

@ -0,0 +1,83 @@
import { DiscordRecord } from "./Record";
export class Channel extends DiscordRecord {
constructor(channel: object);
application_id: number | undefined;
bitrate: number;
defaultAutoArchiveDuration: number | undefined;
flags: number;
guild_id: string;
icon: string;
id: string;
lastMessageId: string;
lastPinTimestamp: string | undefined;
member: unknown;
memberCount: number | undefined;
memberIdsPreview: string[] | undefined;
memberListId: unknown;
messageCount: number | undefined;
name: string;
nicks: Record<string, unknown>;
nsfw: boolean;
originChannelId: unknown;
ownerId: string;
parent_id: string;
permissionOverwrites: {
[role: string]: {
id: string;
type: number;
deny: bigint;
allow: bigint;
};
};
position: number;
rateLimitPerUser: number;
rawRecipients: {
id: string;
avatar: string;
username: string;
public_flags: number;
discriminator: string;
}[];
recipients: string[];
rtcRegion: string;
threadMetadata: {
locked: boolean;
archived: boolean;
invitable: boolean;
createTimestamp: string | undefined;
autoArchiveDuration: number;
archiveTimestamp: string | undefined;
};
topic: string;
type: number;
userLimit: number;
videoQualityMode: undefined;
get accessPermissions(): bigint;
get lastActiveTimestamp(): number;
computeLurkerPermissionsAllowList(): unknown;
getApplicationId(): unknown;
getGuildId(): string;
getRecipientId(): unknown;
hasFlag(flag: number): boolean;
isActiveThread(): boolean;
isArchivedThread(): boolean;
isCategory(): boolean;
isDM(): boolean;
isDirectory(): boolean;
isForumChannel(): boolean;
isGroupDM(): boolean;
isGuildStageVoice(): boolean;
isGuildVoice(): boolean;
isListenModeCapable(): boolean;
isManaged(): boolean;
isMultiUserDM(): boolean;
isNSFW(): boolean;
isOwner(): boolean;
isPrivate(): boolean;
isSystemDM(): boolean;
isThread(): boolean;
isVocal(): boolean;
}

View file

@ -0,0 +1,64 @@
import { Role } from './Role';
import { DiscordRecord } from './Record';
// copy(Object.keys(findByProps("CREATOR_MONETIZABLE")).map(JSON.stringify).join("|"))
export type GuildFeatures =
"INVITE_SPLASH" | "VIP_REGIONS" | "VANITY_URL" | "MORE_EMOJI" | "MORE_STICKERS" | "MORE_SOUNDBOARD" | "VERIFIED" | "COMMERCE" | "DISCOVERABLE" | "COMMUNITY" | "FEATURABLE" | "NEWS" | "HUB" | "PARTNERED" | "ANIMATED_ICON" | "BANNER" | "ENABLED_DISCOVERABLE_BEFORE" | "WELCOME_SCREEN_ENABLED" | "MEMBER_VERIFICATION_GATE_ENABLED" | "PREVIEW_ENABLED" | "ROLE_SUBSCRIPTIONS_ENABLED" | "ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE" | "CREATOR_MONETIZABLE" | "CREATOR_MONETIZABLE_PROVISIONAL" | "CREATOR_MONETIZABLE_WHITEGLOVE" | "CREATOR_MONETIZABLE_DISABLED" | "CREATOR_MONETIZABLE_RESTRICTED" | "CREATOR_STORE_PAGE" | "CREATOR_MONETIZABLE_PENDING_NEW_OWNER_ONBOARDING" | "PRODUCTS_AVAILABLE_FOR_PURCHASE" | "GUILD_WEB_PAGE_VANITY_URL" | "THREADS_ENABLED" | "THREADS_ENABLED_TESTING" | "NEW_THREAD_PERMISSIONS" | "ROLE_ICONS" | "TEXT_IN_STAGE_ENABLED" | "TEXT_IN_VOICE_ENABLED" | "HAS_DIRECTORY_ENTRY" | "ANIMATED_BANNER" | "LINKED_TO_HUB" | "EXPOSED_TO_ACTIVITIES_WTP_EXPERIMENT" | "GUILD_HOME_DEPRECATION_OVERRIDE" | "GUILD_HOME_TEST" | "GUILD_HOME_OVERRIDE" | "GUILD_ONBOARDING" | "GUILD_ONBOARDING_EVER_ENABLED" | "GUILD_ONBOARDING_HAS_PROMPTS" | "GUILD_SERVER_GUIDE" | "INTERNAL_EMPLOYEE_ONLY" | "AUTO_MODERATION" | "INVITES_DISABLED" | "BURST_REACTIONS" | "SOUNDBOARD" | "SHARD" | "ACTIVITY_FEED_ENABLED_BY_USER" | "ACTIVITY_FEED_DISABLED_BY_USER" | "SUMMARIES_ENABLED_GA" | "LEADERBOARD_ENABLED" | "SUMMARIES_ENABLED_BY_USER" | "SUMMARIES_OPT_OUT_EXPERIENCE" | "CHANNEL_ICON_EMOJIS_GENERATED" | "NON_COMMUNITY_RAID_ALERTS" | "RAID_ALERTS_DISABLED" | "AUTOMOD_TRIGGER_USER_PROFILE" | "ENABLED_MODERATION_EXPERIENCE_FOR_NON_COMMUNITY" | "GUILD_PRODUCTS_ALLOW_ARCHIVED_FILE" | "CLAN" | "MEMBER_VERIFICATION_MANUAL_APPROVAL" | "FORWARDING_DISABLED" | "MEMBER_VERIFICATION_ROLLOUT_TEST" | "AUDIO_BITRATE_128_KBPS" | "AUDIO_BITRATE_256_KBPS" | "AUDIO_BITRATE_384_KBPS" | "VIDEO_BITRATE_ENHANCED" | "MAX_FILE_SIZE_50_MB" | "MAX_FILE_SIZE_100_MB" | "GUILD_TAGS" | "ENHANCED_ROLE_COLORS" | "PREMIUM_TIER_3_OVERRIDE" | "REPORT_TO_MOD_PILOT" | "TIERLESS_BOOSTING_SYSTEM_MESSAGE";
export type GuildPremiumFeatures =
"ANIMATED_ICON" | "STAGE_CHANNEL_VIEWERS_150" | "ROLE_ICONS" | "GUILD_TAGS" | "BANNER" | "MAX_FILE_SIZE_50_MB" | "VIDEO_QUALITY_720_60FPS" | "STAGE_CHANNEL_VIEWERS_50" | "VIDEO_QUALITY_1080_60FPS" | "MAX_FILE_SIZE_100_MB" | "VANITY_URL" | "VIDEO_BITRATE_ENHANCED" | "STAGE_CHANNEL_VIEWERS_300" | "AUDIO_BITRATE_128_KBPS" | "ANIMATED_BANNER" | "TIERLESS_BOOSTING" | "ENHANCED_ROLE_COLORS" | "INVITE_SPLASH" | "AUDIO_BITRATE_256_KBPS" | "AUDIO_BITRATE_384_KBPS";
export class Guild extends DiscordRecord {
constructor(guild: object);
afkChannelId: string | undefined;
afkTimeout: number;
applicationCommandCounts: {
0: number;
1: number;
2: number;
};
application_id: unknown;
banner: string | undefined;
defaultMessageNotifications: number;
description: string | undefined;
discoverySplash: string | undefined;
explicitContentFilter: number;
features: Set<GuildFeatures>;
homeHeader: string | undefined;
hubType: unknown;
icon: string | undefined;
id: string;
joinedAt: Date;
latestOnboardingQuestionId: string | undefined;
maxMembers: number;
maxStageVideoChannelUsers: number;
maxVideoChannelUsers: number;
mfaLevel: number;
moderatorReporting: unknown;
name: string;
nsfwLevel: number;
ownerConfiguredContentLevel: number;
ownerId: string;
preferredLocale: string;
premiumFeatures: {
additionalEmojiSlots: number;
additionalSoundSlots: number;
additionalStickerSlots: number;
features: Array<GuildPremiumFeatures>;
};
premiumProgressBarEnabled: boolean;
premiumSubscriberCount: number;
premiumTier: number;
profile: {
badge: string | undefined;
tag: string | undefined;
} | undefined;
publicUpdatesChannelId: string | undefined;
roles: Record<string, Role>;
rulesChannelId: string | undefined;
safetyAlertsChannelId: string | undefined;
splash: string | undefined;
systemChannelFlags: number;
systemChannelId: string | undefined;
vanityURLCode: string | undefined;
verificationLevel: number;
}

View file

@ -0,0 +1,26 @@
export interface GuildMember {
avatar: string | undefined;
avatarDecoration: string | undefined;
banner: string | undefined;
bio: string;
colorRoleId: string | undefined;
colorString: string;
colorStrings: {
primaryColor: string | undefined;
secondaryColor: string | undefined;
tertiaryColor: string | undefined;
};
communicationDisabledUntil: string | undefined;
flags: number;
fullProfileLoadedTimestamp: number;
guildId: string;
highestRoleId: string;
hoistRoleId: string;
iconRoleId: string;
isPending: boolean | undefined;
joinedAt: string | undefined;
nick: string | undefined;
premiumSince: string | undefined;
roles: string[];
userId: string;
}

View file

@ -0,0 +1,12 @@
type Updater = (value: any) => any;
/**
* Common Record class extended by various Discord data structures, like User, Channel, Guild, etc.
*/
export class DiscordRecord {
toJS(): Record<string, any>;
set(key: string, value: any): this;
merge(data: Record<string, any>): this;
update(key: string, defaultValueOrUpdater: Updater | any, updater?: Updater): this;
}

View file

@ -0,0 +1,33 @@
export interface Role {
color: number;
colorString: string | undefined;
colorStrings: {
primaryColor: string | undefined;
secondaryColor: string | undefined;
tertiaryColor: string | undefined;
};
colors: {
primary_color: number | undefined;
secondary_color: number | undefined;
tertiary_color: number | undefined;
};
flags: number;
hoist: boolean;
icon: string | undefined;
id: string;
managed: boolean;
mentionable: boolean;
name: string;
originalPosition: number;
permissions: bigint;
position: number;
/**
* probably incomplete
*/
tags: {
bot_id: string;
integration_id: string;
premium_subscriber: unknown;
} | undefined;
unicodeEmoji: string | undefined;
}

View file

@ -0,0 +1,65 @@
// TODO: a lot of optional params can also be null, not just undef
import { DiscordRecord } from "./Record";
export class User extends DiscordRecord {
constructor(user: object);
accentColor: number;
avatar: string;
banner: string | null | undefined;
bio: string;
bot: boolean;
desktop: boolean;
discriminator: string;
email: string | undefined;
flags: number;
globalName: string | undefined;
guildMemberAvatars: Record<string, string>;
id: string;
mfaEnabled: boolean;
mobile: boolean;
nsfwAllowed: boolean | undefined;
phone: string | undefined;
premiumType: number | undefined;
premiumUsageFlags: number;
publicFlags: number;
purchasedFlags: number;
system: boolean;
username: string;
verified: boolean;
get createdAt(): Date;
get hasPremiumPerks(): boolean;
get tag(): string;
get usernameNormalized(): string;
addGuildAvatarHash(guildId: string, avatarHash: string): User;
getAvatarSource(guildId: string, canAnimate?: boolean): { uri: string; };
getAvatarURL(guildId?: string | null, t?: unknown, canAnimate?: boolean): string;
hasAvatarForGuild(guildId: string): boolean;
hasDisabledPremium(): boolean;
hasFlag(flag: number): boolean;
hasFreePremium(): boolean;
hasHadSKU(e: unknown): boolean;
hasPremiumUsageFlag(flag: number): boolean;
hasPurchasedFlag(flag: number): boolean;
hasUrgentMessages(): boolean;
isClaimed(): boolean;
isLocalBot(): boolean;
isNonUserBot(): boolean;
isPhoneVerified(): boolean;
isStaff(): boolean;
isSystemUser(): boolean;
isVerifiedBot(): boolean;
removeGuildAvatarHash(guildId: string): User;
toString(): string;
}
export interface UserJSON {
avatar: string;
avatarDecoration: unknown | undefined;
discriminator: string;
id: string;
publicFlags: number;
username: string;
}

View file

@ -0,0 +1,9 @@
export * from "./Activity";
export * from "./Application";
export * from "./Channel";
export * from "./Guild";
export * from "./GuildMember";
export * from "./messages";
export * from "./Role";
export * from "./User";
export * from "./Record";

View file

@ -0,0 +1,61 @@
import { Channel } from "../Channel";
import { Guild } from "../Guild";
import { Promisable } from "type-fest";
import { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationCommandType } from "../../../enums";
export interface CommandContext {
channel: Channel;
guild?: Guild;
}
export interface CommandOption {
name: string;
displayName?: string;
type: ApplicationCommandOptionType;
description: string;
displayDescription?: string;
required?: boolean;
options?: CommandOption[];
choices?: Array<ChoicesOption>;
}
export interface ChoicesOption {
label: string;
value: string;
name: string;
displayName?: string;
}
export interface CommandReturnValue {
content: string;
// TODO: implement
// cancel?: boolean;
}
export interface CommandArgument {
type: ApplicationCommandOptionType;
name: string;
value: string;
focused: undefined;
options: CommandArgument[];
}
export interface Command {
id?: string;
applicationId?: string;
type?: ApplicationCommandType;
inputType?: ApplicationCommandInputType;
plugin?: string;
name: string;
untranslatedName?: string;
displayName?: string;
description: string;
untranslatedDescription?: string;
displayDescription?: string;
options?: CommandOption[];
predicate?(ctx: CommandContext): boolean;
execute(args: CommandArgument[], ctx: CommandContext): Promisable<void | CommandReturnValue>;
}

View file

@ -0,0 +1,70 @@
export interface Embed {
author?: {
name: string;
url: string;
iconURL: string | undefined;
iconProxyURL: string | undefined;
};
color: string;
fields: [];
id: string;
image?: {
height: number;
width: number;
url: string;
proxyURL: string;
};
provider?: {
name: string;
url: string | undefined;
};
rawDescription: string;
rawTitle: string;
referenceId: unknown;
timestamp: string;
thumbnail?: {
height: number;
proxyURL: string | undefined;
url: string;
width: number;
};
type: string;
url: string | undefined;
video?: {
height: number;
width: number;
url: string;
proxyURL: string | undefined;
};
}
export interface EmbedJSON {
author?: {
name: string;
url: string;
icon_url: string;
proxy_icon_url: string;
};
title: string;
color: string;
description: string;
type: string;
url: string | undefined;
provider?: {
name: string;
url: string;
};
timestamp: string;
thumbnail?: {
height: number;
width: number;
url: string;
proxy_url: string | undefined;
};
video?: {
height: number;
width: number;
url: string;
proxy_url: string | undefined;
};
}

View file

@ -0,0 +1,42 @@
export type Emoji = CustomEmoji | UnicodeEmoji;
export interface CustomEmoji {
type: 1;
allNamesString: string;
animated: boolean;
available: boolean;
guildId: string;
id: string;
managed: boolean;
name: string;
originalName?: string;
require_colons: boolean;
roles: string[];
}
export interface UnicodeEmoji {
type: 0;
diversityChildren: Record<any, any>;
emojiObject: {
names: string[];
surrogates: string;
unicodeVersion: number;
};
index: number;
surrogates: string;
uniqueName: string;
useSpriteSheet: boolean;
get allNamesString(): string;
get animated(): boolean;
get defaultDiversityChild(): any;
get hasDiversity(): boolean | undefined;
get hasDiversityParent(): boolean | undefined;
get hasMultiDiversity(): boolean | undefined;
get hasMultiDiversityParent(): boolean | undefined;
get managed(): boolean;
get name(): string;
get names(): string[];
get optionallyDiverseSequence(): string | undefined;
get unicodeVersion(): number;
get url(): string;
}

View file

@ -0,0 +1,204 @@
import { CommandOption } from './Commands';
import { User, UserJSON } from '../User';
import { Embed, EmbedJSON } from './Embed';
import { DiscordRecord } from "../Record";
import { MessageFlags, MessageType, StickerFormatType } from "../../../enums";
/*
* TODO: looks like discord has moved over to Date instead of Moment;
*/
export class Message extends DiscordRecord {
constructor(message: object);
activity: unknown;
application: unknown;
applicationId: string | unknown;
attachments: MessageAttachment[];
author: User;
blocked: boolean;
bot: boolean;
call: {
duration: moment.Duration;
endedTimestamp: moment.Moment;
participants: string[];
};
channel_id: string;
/**
* NOTE: not fully typed
*/
codedLinks: {
code?: string;
type: string;
}[];
colorString: unknown;
components: unknown[];
content: string;
customRenderedContent: unknown;
editedTimestamp: Date;
embeds: Embed[];
flags: MessageFlags;
giftCodes: string[];
id: string;
interaction: {
id: string;
name: string;
type: number;
user: User;
}[] | undefined;
interactionData: {
application_command: {
application_id: string;
default_member_permissions: unknown;
default_permission: boolean;
description: string;
dm_permission: unknown;
id: string;
name: string;
options: CommandOption[];
permissions: unknown[];
type: number;
version: string;
};
attachments: MessageAttachment[];
guild_id: string | undefined;
id: string;
name: string;
options: {
focused: unknown;
name: string;
type: number;
value: string;
}[];
type: number;
version: string;
}[];
interactionError: unknown[];
isSearchHit: boolean;
loggingName: unknown;
mentionChannels: string[];
mentionEveryone: boolean;
mentionRoles: string[];
mentioned: boolean;
mentions: string[];
messageReference: {
guild_id?: string;
channel_id: string;
message_id: string;
} | undefined;
messageSnapshots: {
message: Message;
}[];
nick: unknown; // probably a string
nonce: string | undefined;
pinned: boolean;
reactions: MessageReaction[];
state: string;
stickerItems: {
format_type: StickerFormatType;
id: string;
name: string;
}[];
stickers: unknown[];
timestamp: moment.Moment;
tts: boolean;
type: MessageType;
webhookId: string | undefined;
/**
* Doesn't actually update the original message; it just returns a new message instance with the added reaction.
*/
addReaction(emoji: ReactionEmoji, fromCurrentUser: boolean): Message;
/**
* Searches each reaction and if the provided string has an index above -1 it'll return the reaction object.
*/
getReaction(name: string): MessageReaction;
/**
* Doesn't actually update the original message; it just returns the message instance without the reaction searched with the provided emoji object.
*/
removeReactionsForEmoji(emoji: ReactionEmoji): Message;
/**
* Doesn't actually update the original message; it just returns the message instance without the reaction.
*/
removeReaction(emoji: ReactionEmoji, fromCurrentUser: boolean): Message;
getChannelId(): string;
hasFlag(flag: MessageFlags): boolean;
isCommandType(): boolean;
isEdited(): boolean;
isSystemDM(): boolean;
/** Vencord added */
deleted?: boolean;
}
/** A smaller Message object found in FluxDispatcher and elsewhere. */
export interface MessageJSON {
attachments: MessageAttachment[];
author: UserJSON;
channel_id: string;
components: unknown[];
content: string;
edited_timestamp: string;
embeds: EmbedJSON[];
flags: number;
guild_id: string | undefined;
id: string;
loggingName: unknown;
member: {
avatar: string | undefined;
communication_disabled_until: string | undefined;
deaf: boolean;
hoisted_role: string | undefined;
is_pending: boolean;
joined_at: string;
mute: boolean;
nick: string | boolean;
pending: boolean;
premium_since: string | undefined;
roles: string[];
} | undefined;
mention_everyone: boolean;
mention_roles: string[];
mentions: UserJSON[];
message_reference: {
guild_id?: string;
channel_id: string;
message_id: string;
} | undefined;
nonce: string | undefined;
pinned: boolean;
referenced_message: MessageJSON | undefined;
state: string;
timestamp: string;
tts: boolean;
type: number;
}
export interface MessageAttachment {
filename: string;
id: string;
proxy_url: string;
size: number;
spoiler: boolean;
url: string;
content_type?: string;
width?: number;
height?: number;
}
export interface ReactionEmoji {
id: string | undefined;
name: string;
animated: boolean;
}
export interface MessageReaction {
count: number;
emoji: ReactionEmoji;
me: boolean;
}
// Object.keys(findByProps("REPLYABLE")).map(JSON.stringify).join("|")
export type MessageTypeSets = Record<
"UNDELETABLE" | "GUILD_DISCOVERY_STATUS" | "USER_MESSAGE" | "NOTIFIABLE_SYSTEM_MESSAGE" | "REPLYABLE" | "FORWARDABLE" | "REFERENCED_MESSAGE_AVAILABLE" | "AVAILABLE_IN_GUILD_FEED" | "DEADCHAT_PROMPTS" | "NON_COLLAPSIBLE" | "NON_PARSED" | "AUTOMOD_INCIDENT_ACTIONS" | "SELF_MENTIONABLE_SYSTEM" | "SCHEDULABLE",
Set<MessageType>
>;

View file

@ -0,0 +1,35 @@
import { StickerFormatType, StickerType } from "../../../enums";
interface BaseSticker {
asset: string;
available: boolean;
description: string;
format_type: StickerFormatType;
id: string;
name: string;
sort_value?: number;
/** a comma separated string */
tags: string;
}
export interface PackSticker extends BaseSticker {
pack_id: string;
type: StickerType.STANDARD;
}
export interface GuildSticker extends BaseSticker {
guild_id: string;
type: StickerType.GUILD;
}
export type Sticker = PackSticker | GuildSticker;
export interface PremiumStickerPack {
banner_asset_id?: string;
cover_sticker_id?: string;
description: string;
id: string;
name: string;
sku_id: string;
stickers: PackSticker[];
}

View file

@ -0,0 +1,5 @@
export * from "./Commands";
export * from "./Message";
export * from "./Embed";
export * from "./Emoji";
export * from "./Sticker";

View file

@ -1,25 +1,7 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import type { ComponentClass, ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PointerEvent, PropsWithChildren, ReactNode, Ref, RefObject } from "react";
export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/bold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/bold" | "eyebrow" | "heading-deprecated-14/normal" | "heading-deprecated-14/medium" | "heading-deprecated-14/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "display-sm" | "display-md" | "display-lg" | "code";
// copy(find(m => Array.isArray(m) && m.includes("heading-sm/normal")).map(JSON.stringify).join("|"))
export type TextVariant = "heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-sm/extrabold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-md/extrabold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-lg/extrabold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/semibold" | "heading-xl/bold" | "heading-xl/extrabold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/semibold" | "heading-xxl/bold" | "heading-xxl/extrabold" | "eyebrow" | "heading-deprecated-12/normal" | "heading-deprecated-12/medium" | "heading-deprecated-12/semibold" | "heading-deprecated-12/bold" | "heading-deprecated-12/extrabold" | "redesign/heading-18/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "redesign/message-preview/normal" | "redesign/message-preview/medium" | "redesign/message-preview/semibold" | "redesign/message-preview/bold" | "redesign/channel-title/normal" | "redesign/channel-title/medium" | "redesign/channel-title/semibold" | "redesign/channel-title/bold" | "display-sm" | "display-md" | "display-lg" | "code";
export type FormTextTypes = Record<"DEFAULT" | "INPUT_PLACEHOLDER" | "DESCRIPTION" | "LABEL_BOLD" | "LABEL_SELECTED" | "LABEL_DESCRIPTOR" | "ERROR" | "SUCCESS", string>;
export type HeadingTag = `h${1 | 2 | 3 | 4 | 5 | 6}`;
@ -61,12 +43,7 @@ export type FormDivider = ComponentType<{
}>;
export type FormText = ComponentType<PropsWithChildren<{
disabled?: boolean;
selectable?: boolean;
/** defaults to FormText.Types.DEFAULT */
type?: string;
}> & TextProps> & { Types: FormTextTypes; };
export type FormText = ComponentType<TextProps>;
export type Tooltip = ComponentType<{
text: ReactNode | ComponentType;
@ -262,8 +239,10 @@ export type TextInput = ComponentType<PropsWithChildren<{
Sizes: Record<"DEFAULT" | "MINI", string>;
};
// FIXME: this is wrong, it's not actually just HTMLTextAreaElement
export type TextArea = ComponentType<Omit<HTMLProps<HTMLTextAreaElement>, "onChange"> & {
onChange(v: string): void;
inputRef?: Ref<HTMLTextAreaElement>;
}>;
interface SelectOption {
@ -490,19 +469,66 @@ export type MaskedLink = ComponentType<PropsWithChildren<{
channelId?: string;
}>>;
export type ScrollerThin = ComponentType<PropsWithChildren<{
export interface ScrollerBaseProps {
className?: string;
style?: CSSProperties;
dir?: "ltr";
orientation?: "horizontal" | "vertical" | "auto";
paddingFix?: boolean;
fade?: boolean;
onClose?(): void;
onScroll?(): void;
}
export type ScrollerThin = ComponentType<PropsWithChildren<ScrollerBaseProps & {
orientation?: "horizontal" | "vertical" | "auto";
fade?: boolean;
}>>;
interface BaseListItem {
anchorId: any;
listIndex: number;
offsetTop: number;
section: number;
}
interface ListSection extends BaseListItem {
type: "section";
}
interface ListRow extends BaseListItem {
type: "row";
row: number;
rowIndex: number;
}
export type ListScrollerThin = ComponentType<ScrollerBaseProps & {
sections: number[];
renderSection?: (item: ListSection) => React.ReactNode;
renderRow: (item: ListRow) => React.ReactNode;
renderFooter?: (item: any) => React.ReactNode;
renderSidebar?: (listVisible: boolean, sidebarVisible: boolean) => React.ReactNode;
wrapSection?: (section: number, children: React.ReactNode) => React.ReactNode;
sectionHeight: number;
rowHeight: number;
footerHeight?: number;
sidebarHeight?: number;
chunkSize?: number;
paddingTop?: number;
paddingBottom?: number;
fade?: boolean;
onResize?: Function;
getAnchorId?: any;
innerTag?: string;
innerId?: string;
innerClassName?: string;
innerRole?: string;
innerAriaLabel?: string;
// Yes, Discord uses this casing
innerAriaMultiselectable?: boolean;
innerAriaOrientation?: "vertical" | "horizontal";
}>;
export type Clickable = <T extends "a" | "div" | "span" | "li" = "div">(props: PropsWithChildren<ComponentPropsWithRef<T>> & {
tag?: T;
}) => ReactNode;

30
packages/discord-types/src/flux.d.ts vendored Normal file
View file

@ -0,0 +1,30 @@
import { FluxStore } from "./stores/FluxStore";
export class FluxEmitter {
constructor();
changeSentinel: number;
changedStores: Set<FluxStore>;
isBatchEmitting: boolean;
isDispatching: boolean;
isPaused: boolean;
pauseTimer: NodeJS.Timeout | null;
reactChangedStores: Set<FluxStore>;
batched(batch: (...args: any[]) => void): void;
destroy(): void;
emit(): void;
emitNonReactOnce(): void;
emitReactOnce(): void;
getChangeSentinel(): number;
getIsPaused(): boolean;
injectBatchEmitChanges(batch: (...args: any[]) => void): void;
markChanged(store: FluxStore): void;
pause(): void;
resume(): void;
}
export interface Flux {
Store: typeof FluxStore;
Emitter: FluxEmitter;
}

File diff suppressed because one or more lines are too long

10
packages/discord-types/src/index.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
export * from "./common";
export * from "./classes";
export * from "./components";
export * from "./flux";
export * from "./fluxEvents";
export * from "./menu";
export * from "./modules";
export * from "./stores";
export * from "./utils";
export * as Webpack from "../webpack";

View file

@ -1,21 +1,3 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import type { ComponentType, CSSProperties, MouseEvent, PropsWithChildren, ReactNode, UIEvent } from "react";
type RC<C> = ComponentType<PropsWithChildren<C & Record<string, any>>>;
@ -73,7 +55,7 @@ export interface Menu {
renderValue?(value: number): string,
}>;
MenuSearchControl: RC<{
query: string
query: string;
onChange(query: string): void;
placeholder?: string;
}>;

View file

@ -0,0 +1,74 @@
import EventEmitter from "events";
import { CloudUploadPlatform } from "../../enums";
interface BaseUploadItem {
platform: CloudUploadPlatform;
id?: string;
origin?: string;
isThumbnail?: boolean;
clip?: unknown;
}
export interface ReactNativeUploadItem extends BaseUploadItem {
platform: CloudUploadPlatform.REACT_NATIVE;
uri: string;
filename?: string;
mimeType?: string;
durationSecs?: number;
waveform?: string;
isRemix?: boolean;
}
export interface WebUploadItem extends BaseUploadItem {
platform: CloudUploadPlatform.WEB;
file: File;
}
export type CloudUploadItem = ReactNativeUploadItem | WebUploadItem;
export class CloudUpload extends EventEmitter {
constructor(item: CloudUploadItem, channelId: string, showLargeMessageDialog?: boolean, reactNativeFileIndex?: number);
channelId: string;
classification: string;
clip: unknown;
contentHash: unknown;
currentSize: number;
description: string | null;
durationSecs: number | undefined;
etag: string | undefined;
error: unknown;
filename: string;
id: string;
isImage: boolean;
isRemix: boolean | undefined;
isThumbnail: boolean;
isVideo: boolean;
item: {
file: File;
platform: CloudUploadPlatform;
origin: string;
};
loaded: number;
mimeType: string;
origin: string;
postCompressionSize: number | undefined;
preCompressionSize: number;
responseUrl: string;
sensitive: boolean;
showLargeMessageDialog: boolean;
spoiler: boolean;
startTime: number;
status: "NOT_STARTED" | "STARTED" | "UPLOADING" | "ERROR" | "COMPLETED" | "CANCELLED" | "REMOVED_FROM_MSG_DRAFT";
uniqueId: string;
uploadedFilename: string;
waveform: string | undefined;
// there are many more methods than just these but I didn't find them particularly useful
upload(): Promise<void>;
cancel(): void;
delete(): Promise<void>;
getSize(): number;
maybeConvertToWebP(): Promise<void>;
removeFromMsgDraft(): void;
}

View file

@ -0,0 +1 @@
export * from "./CloudUpload";

View file

@ -0,0 +1,11 @@
import { FluxStore } from "..";
export class AuthenticationStore extends FluxStore {
/**
* Gets the id of the current user
*/
getId(): string;
// This Store has a lot more methods related to everything Auth, but they really should
// not be needed, so they are not typed
}

View file

@ -0,0 +1,24 @@
import { Channel, FluxStore } from "..";
export class ChannelStore extends FluxStore {
getChannel(channelId: string): Channel;
getBasicChannel(channelId: string): Channel | undefined;
hasChannel(channelId: string): boolean;
getChannelIds(guildId?: string | null): string[];
getMutableBasicGuildChannelsForGuild(guildId: string): Record<string, Channel>;
getMutableGuildChannelsForGuild(guildId: string): Record<string, Channel>;
getAllThreadsForGuild(guildId: string): Channel[];
getAllThreadsForParent(channelId: string): Channel[];
getDMFromUserId(userId: string): string;
getDMChannelFromUserId(userId: string): Channel | undefined;
getDMUserIds(): string[];
getMutableDMsByUserIds(): Record<string, string>;
getMutablePrivateChannels(): Record<string, Channel>;
getSortedPrivateChannels(): Channel[];
getGuildChannelsVersion(guildId: string): number;
getPrivateChannelsVersion(): number;
getInitialOverlayState(): Record<string, Channel>;
}

View file

@ -0,0 +1,43 @@
import { FluxStore } from "..";
export enum DraftType {
ChannelMessage = 0,
ThreadSettings = 1,
FirstThreadMessage = 2,
ApplicationLauncherCommand = 3,
Poll = 4,
SlashCommand = 5,
ForwardContextMessage = 6
}
export interface Draft {
timestamp: number;
draft: string;
}
export interface ThreadSettingsDraft {
timestamp: number;
parentMessageId?: string;
name?: string;
isPrivate?: boolean;
parentChannelId?: string;
location?: string;
}
export type ChannelDrafts = {
[DraftType.ThreadSettings]: ThreadSettingsDraft;
} & {
[key in Exclude<DraftType, DraftType.ThreadSettings>]: Draft;
};
export type UserDrafts = Partial<Record<string, ChannelDrafts>>;
export type DraftState = Partial<Record<string, UserDrafts>>;
export class DraftStore extends FluxStore {
getState(): DraftState;
getRecentlyEditedDrafts(type: DraftType): Array<Draft & { channelId: string; }>;
getDraft(channelId: string, type: DraftType): string;
getThreadSettings(channelId: string): ThreadSettingsDraft | null | undefined;
getThreadDraftWithParentMessageId(parentMessageId: string): ThreadSettingsDraft | null | undefined;
}

View file

@ -0,0 +1,57 @@
import { Channel, CustomEmoji, Emoji, FluxStore } from "..";
export class EmojiStore extends FluxStore {
getCustomEmojiById(id?: string | null): CustomEmoji | undefined;
getUsableCustomEmojiById(id?: string | null): CustomEmoji | undefined;
getGuilds(): Record<string, {
id: string;
get emojis(): CustomEmoji[];
get rawEmojis(): CustomEmoji[];
get usableEmojis(): CustomEmoji[];
get emoticons(): any[];
getEmoji(id: string): CustomEmoji | undefined;
isUsable(emoji: CustomEmoji): boolean;
}>;
getGuildEmoji(guildId?: string | null): CustomEmoji[];
getNewlyAddedEmoji(guildId?: string | null): CustomEmoji[];
getTopEmoji(guildId?: string | null): CustomEmoji[];
getTopEmojisMetadata(guildId?: string | null): {
emojiIds: string[];
topEmojisTTL: number;
};
hasPendingUsage(): boolean;
hasUsableEmojiInAnyGuild(): boolean;
searchWithoutFetchingLatest(data: any): any;
getSearchResultsOrder(...args: any[]): any;
getState(): {
pendingUsages: { key: string, timestamp: number; }[];
};
searchWithoutFetchingLatest(data: {
channel: Channel;
query: string;
count?: number;
intention: number;
includeExternalGuilds?: boolean;
matchComparator?(name: string): boolean;
}): Record<"locked" | "unlocked", Emoji[]>;
getDisambiguatedEmojiContext(): {
backfillTopEmojis: Record<any, any>;
customEmojis: Record<string, CustomEmoji>;
emojisById: Record<string, CustomEmoji>;
emojisByName: Record<string, CustomEmoji>;
emoticonRegex: RegExp | null;
emoticonsByName: Record<string, any>;
escapedEmoticonNames: string;
favoriteNamesAndIds?: any;
favorites?: any;
frequentlyUsed?: any;
groupedCustomEmojis: Record<string, CustomEmoji[]>;
guildId?: string;
isFavoriteEmojiWithoutFetchingLatest(e: Emoji): boolean;
newlyAddedEmoji: Record<string, CustomEmoji[]>;
topEmojis?: any;
unicodeAliases: Record<string, string>;
get favoriteEmojisWithoutFetchingLatest(): Emoji[];
};
}

View file

@ -0,0 +1,44 @@
import { FluxDispatcher, FluxEvents } from "..";
type Callback = () => void;
/*
For some reason, this causes type errors when you try to destructure it:
```ts
interface FluxEvent {
type: FluxEvents;
[key: string]: any;
}
```
*/
export type FluxEvent = any;
export type ActionHandler = (event: FluxEvent) => void;
export type ActionHandlers = Partial<Record<FluxEvents, ActionHandler>>;
export class FluxStore {
constructor(dispatcher: FluxDispatcher, actionHandlers?: ActionHandlers);
getName(): string;
addChangeListener(callback: Callback): void;
/** Listener will be removed once the callback returns false. */
addConditionalChangeListener(callback: () => boolean, preemptive?: boolean): void;
addReactChangeListener(callback: Callback): void;
removeChangeListener(callback: Callback): void;
removeReactChangeListener(callback: Callback): void;
doEmitChanges(event: FluxEvent): void;
emitChange(): void;
getDispatchToken(): string;
initialize(): void;
initializeIfNeeded(): void;
/** this is a setter */
mustEmitChanges(actionHandler: ActionHandler | undefined): void;
registerActionHandlers(actionHandlers: ActionHandlers): void;
syncWith(stores: FluxStore[], callback: Callback, timeout?: number): void;
waitFor(...stores: FluxStore[]): void;
static getAll(): FluxStore[];
}

View file

@ -0,0 +1,27 @@
import { FluxStore, GuildMember } from "..";
export class GuildMemberStore extends FluxStore {
/** @returns Format: [guildId-userId: Timestamp (string)] */
getCommunicationDisabledUserMap(): Record<string, string>;
getCommunicationDisabledVersion(): number;
getMutableAllGuildsAndMembers(): Record<string, Record<string, GuildMember>>;
getMember(guildId: string, userId: string): GuildMember | null;
getTrueMember(guildId: string, userId: string): GuildMember | null;
getMemberIds(guildId: string): string[];
getMembers(guildId: string): GuildMember[];
getCachedSelfMember(guildId: string): GuildMember | null;
getSelfMember(guildId: string): GuildMember | null;
getSelfMemberJoinedAt(guildId: string): Date | null;
getNick(guildId: string, userId: string): string | null;
getNicknameGuildsMapping(userId: string): Record<string, string[]>;
getNicknames(userId: string): string[];
isMember(guildId: string, userId: string): boolean;
isMember(guildId: string, userId: string): boolean;
isGuestOrLurker(guildId: string, userId: string): boolean;
isCurrentUserGuest(guildId: string): boolean;
}

View file

@ -0,0 +1,8 @@
import { FluxStore, Role } from "..";
// TODO: add the rest of the methods for GuildRoleStore
export class GuildRoleStore extends FluxStore {
getRole(guildId: string, roleId: string): Role;
getSortedRoles(guildId: string): Role[];
getRolesSnapshot(guildId: string): Record<string, Role>;
}

View file

@ -0,0 +1,8 @@
import { Guild, FluxStore } from "..";
export class GuildStore extends FluxStore {
getGuild(guildId: string): Guild;
getGuildCount(): number;
getGuilds(): Record<string, Guild>;
getGuildIds(): string[];
}

View file

@ -0,0 +1,13 @@
import { MessageJSON, FluxStore, Message } from "..";
export class MessageStore extends FluxStore {
getMessage(channelId: string, messageId: string): Message;
/** @returns This return object is fucking huge; I'll type it later. */
getMessages(channelId: string): unknown;
getRawMessages(channelId: string): Record<string | number, MessageJSON>;
hasCurrentUserSentMessage(channelId: string): boolean;
hasPresent(channelId: string): boolean;
isLoadingMessages(channelId: string): boolean;
jumpedMessageId(channelId: string): string | undefined;
whenReady(channelId: string, callback: () => void): void;
}

View file

@ -0,0 +1,26 @@
import { FluxStore } from "..";
export class RelationshipStore extends FluxStore {
getFriendIDs(): string[];
getIgnoredIDs(): string[];
getBlockedIDs(): string[];
getPendingCount(): number;
getRelationshipCount(): number;
/** Related to friend nicknames. */
getNickname(userId: string): string;
/** @returns Enum value from constants.RelationshipTypes */
getRelationshipType(userId: string): number;
isFriend(userId: string): boolean;
isBlocked(userId: string): boolean;
isIgnored(userId: string): boolean;
/**
* @see {@link isBlocked}
* @see {@link isIgnored}
*/
isBlockedOrIgnored(userId: string): boolean;
getSince(userId: string): string;
getMutableRelationships(): Map<string, number>;
}

View file

@ -0,0 +1,14 @@
import { FluxStore } from "..";
export class SelectedChannelStore extends FluxStore {
getChannelId(guildId?: string | null): string;
getVoiceChannelId(): string | undefined;
getCurrentlySelectedChannelId(guildId?: string): string | undefined;
getMostRecentSelectedTextChannelId(guildId: string): string | undefined;
getLastSelectedChannelId(guildId?: string): string;
// yes this returns a string
getLastSelectedChannels(guildId?: string): string;
/** If you follow an announcement channel, this will return whichever channel you chose as destination */
getLastChannelFollowingDestination(): { guildId?: string; channelId?: string; } | undefined;
}

View file

@ -0,0 +1,14 @@
import { FluxStore } from "..";
export interface SelectedGuildState {
selectedGuildTimestampMillis: Record<string | number, number>;
selectedGuildId: string | null;
lastSelectedGuildId: string | null;
}
export class SelectedGuildStore extends FluxStore {
getGuildId(): string | null;
getLastSelectedGuildId(): string | null;
getLastSelectedTimestamp(guildId: string): number | null;
getState(): SelectedGuildState | undefined;
}

View file

@ -0,0 +1,15 @@
import { FluxStore, GuildSticker, PremiumStickerPack, Sticker } from "..";
export type StickerGuildMap = Map<string, GuildSticker[]>;
export class StickersStore extends FluxStore {
getAllGuildStickers(): StickerGuildMap;
getRawStickersByGuild(): StickerGuildMap;
getPremiumPacks(): PremiumStickerPack[];
getStickerById(id: string): Sticker | undefined;
getStickerPack(id: string): PremiumStickerPack | undefined;
getStickersByGuildId(guildId: string): Sticker[] | undefined;
isPremiumPack(id: string): boolean;
}

View file

@ -0,0 +1,11 @@
import { FluxStore } from "@vencord/discord-types";
export class StreamerModeStore extends FluxStore {
get autoToggle(): boolean;
get disableNotifications(): boolean;
get disableSounds(): boolean;
get enableContentProtection(): boolean;
get enabled(): boolean;
get hideInstantInvites(): boolean;
get hidePersonalInformation(): boolean;
}

View file

@ -0,0 +1,18 @@
import { FluxStore } from "..";
export type ThemePreference = "dark" | "light" | "unknown";
export type SystemTheme = "dark" | "light";
export type Theme = "light" | "dark" | "darker" | "midnight";
export interface ThemeState {
theme: Theme;
status: 0 | 1;
preferences: Record<ThemePreference, Theme>;
}
export class ThemeStore extends FluxStore {
get theme(): Theme;
get darkSidebar(): boolean;
get systemTheme(): SystemTheme;
themePreferenceForSystemTheme(preference: ThemePreference): Theme;
getState(): ThemeState;
}

View file

@ -0,0 +1,9 @@
import { FluxStore } from "..";
export class TypingStore extends FluxStore {
/**
* returns a map of user ids to timeout ids
*/
getTypingUsers(channelId: string): Record<string, number>;
isTyping(channelId: string, userId: string): boolean;
}

View file

@ -0,0 +1,151 @@
import { FluxStore, Guild, User, Application, ApplicationInstallParams } from "..";
import { ApplicationIntegrationType } from "../../enums";
export interface MutualFriend {
/**
* the userid of the mutual friend
*/
key: string;
/**
* the status of the mutual friend
*/
status: "online" | "offline" | "idle" | "dnd";
/**
* the user object of the mutual friend
*/
user: User;
}
export interface MutualGuild {
/**
* the guild object of the mutual guild
*/
guild: Guild;
/**
* the user's nickname in the guild, if any
*/
nick: string | null;
}
export interface ProfileBadge {
id: string;
description: string;
icon: string;
link?: string;
}
export interface ConnectedAccount {
type: "twitch" | "youtube" | "skype" | "steam" | "leagueoflegends" | "battlenet" | "bluesky" | "bungie" | "reddit" | "twitter" | "twitter_legacy" | "spotify" | "facebook" | "xbox" | "samsung" | "contacts" | "instagram" | "mastodon" | "soundcloud" | "github" | "playstation" | "playstation-stg" | "epicgames" | "riotgames" | "roblox" | "paypal" | "ebay" | "tiktok" | "crunchyroll" | "domain" | "amazon-music";
/**
* underlying id of connected account
* eg. account uuid
*/
id: string;
/**
* display name of connected account
*/
name: string;
verified: boolean;
metadata?: Record<string, string>;
}
export interface ProfileApplication {
id: string;
customInstallUrl: string | undefined;
installParams: ApplicationInstallParams | undefined;
flags: number;
popularApplicationCommandIds?: string[];
integrationTypesConfig: Record<ApplicationIntegrationType, Partial<{
oauth2_install_params: ApplicationInstallParams;
}>>;
primarySkuId: string | undefined;
storefront_available: boolean;
}
export interface UserProfileBase extends Pick<User, "banner"> {
accentColor: number | null;
/**
* often empty for guild profiles, get the user profile for badges
*/
badges: ProfileBadge[];
bio: string | undefined;
popoutAnimationParticleType: string | null;
profileEffectExpiresAt: number | Date | undefined;
profileEffectId: undefined | string;
/**
* often an empty string when not set
*/
pronouns: string | "" | undefined;
themeColors: [number, number] | undefined;
userId: string;
}
export interface ApplicationRoleConnection {
application: Application;
application_metadata: Record<string, any>;
metadata: Record<string, any>;
platform_name: string;
platform_username: string;
}
export interface UserProfile extends UserProfileBase, Pick<User, "premiumType"> {
/** If this is a bot user profile, this will be its application */
application: ProfileApplication | null;
applicationRoleConnections: ApplicationRoleConnection[] | undefined;
connectedAccounts: ConnectedAccount[] | undefined;
fetchStartedAt: number;
fetchEndedAt: number;
legacyUsername: string | undefined;
premiumGuildSince: Date | null;
premiumSince: Date | null;
}
export class UserProfileStore extends FluxStore {
/**
* @param userId the user ID of the profile being fetched.
* @param guildId the guild ID to of the profile being fetched.
* defaults to the internal symbol `NO GUILD ID` if nullish
*
* @returns true if the profile is being fetched, false otherwise.
*/
isFetchingProfile(userId: string, guildId?: string): boolean;
/**
* Check if mutual friends for {@link userId} are currently being fetched.
*
* @param userId the user ID of the mutual friends being fetched.
*
* @returns true if mutual friends are being fetched, false otherwise.
*/
isFetchingFriends(userId: string): boolean;
get isSubmitting(): boolean;
getUserProfile(userId: string): UserProfile | undefined;
getGuildMemberProfile(userId: string, guildId: string | undefined): UserProfileBase | null;
/**
* Get the mutual friends of a user.
*
* @param userId the user ID of the user to get the mutual friends of.
*
* @returns an array of mutual friends, or undefined if the user has no mutual friends
*/
getMutualFriends(userId: string): MutualFriend[] | undefined;
/**
* Get the count of mutual friends for a user.
*
* @param userId the user ID of the user to get the mutual friends count of.
*
* @returns the count of mutual friends, or undefined if the user has no mutual friends
*/
getMutualFriendsCount(userId: string): number | undefined;
/**
* Get the mutual guilds of a user.
*
* @param userId the user ID of the user to get the mutual guilds of.
*
* @returns an array of mutual guilds, or undefined if the user has no mutual guilds
*/
getMutualGuilds(userId: string): MutualGuild[] | undefined;
}

View file

@ -0,0 +1,10 @@
import { FluxStore, User } from "..";
export class UserStore extends FluxStore {
filter(filter: (user: User) => boolean, sort?: boolean): Record<string, User>;
findByTag(username: string, discriminator: string): User;
forEach(action: (user: User) => void): void;
getCurrentUser(): User;
getUser(userId: string): User;
getUsers(): Record<string, User>;
}

View file

@ -0,0 +1,42 @@
import { DiscordRecord } from "../common";
import { FluxStore } from "./FluxStore";
export type UserVoiceStateRecords = Record<string, VoiceState>;
export type VoiceStates = Record<string, UserVoiceStateRecords>;
export interface VoiceState extends DiscordRecord {
userId: string;
channelId: string | null | undefined;
sessionId: string | null | undefined;
mute: boolean;
deaf: boolean;
selfMute: boolean;
selfDeaf: boolean;
selfVideo: boolean;
selfStream: boolean | undefined;
suppress: boolean;
requestToSpeakTimestamp: string | null | undefined;
discoverable: boolean;
isVoiceMuted(): boolean;
isVoiceDeafened(): boolean;
}
export class VoiceStateStore extends FluxStore {
getAllVoiceStates(): VoiceStates;
getVoiceStates(guildId?: string | null): UserVoiceStateRecords;
getVoiceStatesForChannel(channelId: string): UserVoiceStateRecords;
getVideoVoiceStatesForChannel(channelId: string): UserVoiceStateRecords;
getVoiceState(guildId: string | null, userId: string): VoiceState | undefined;
getUserVoiceChannelId(guildId: string | null, userId: string): string | undefined;
getVoiceStateForChannel(channelId: string, userId?: string): VoiceState | undefined;
getVoiceStateForUser(userId: string): VoiceState | undefined;
getCurrentClientVoiceChannelId(guildId: string | null): string | undefined;
isCurrentClientInVoiceChannel(): boolean;
isInChannel(channelId: string, userId?: string): boolean;
hasVideo(channelId: string): boolean;
}

View file

@ -0,0 +1,7 @@
import { FluxStore } from "..";
export class WindowStore extends FluxStore {
isElementFullScreen(): boolean;
isFocused(): boolean;
windowSize(): Record<"width" | "height", number>;
}

View file

@ -0,0 +1,38 @@
// please keep in alphabetical order
export * from "./AuthenticationStore";
export * from "./ChannelStore";
export * from "./DraftStore";
export * from "./EmojiStore";
export * from "./FluxStore";
export * from "./GuildMemberStore";
export * from "./GuildRoleStore";
export * from "./GuildStore";
export * from "./MessageStore";
export * from "./RelationshipStore";
export * from "./SelectedChannelStore";
export * from "./SelectedGuildStore";
export * from "./StickersStore";
export * from "./StreamerModeStore";
export * from "./ThemeStore";
export * from "./TypingStore";
export * from "./UserProfileStore";
export * from "./UserStore";
export * from "./VoiceStateStore";
export * from "./WindowStore";
/**
* React hook that returns stateful data for one or more stores
* You might need a custom comparator (4th argument) if your store data is an object
* @param stores The stores to listen to
* @param mapper A function that returns the data you need
* @param dependencies An array of reactive values which the hook depends on. Use this if your mapper or equality function depends on the value of another hook
* @param isEqual A custom comparator for the data returned by mapper
*
* @example const user = useStateFromStores([UserStore], () => UserStore.getCurrentUser(), null, (old, current) => old.id === current.id);
*/
export type useStateFromStores = <T>(
stores: any[],
mapper: () => T,
dependencies?: any,
isEqual?: (old: T, newer: T) => boolean
) => T;

View file

@ -1,22 +1,4 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import { Channel, Guild, GuildMember, Message, User } from "discord-types/general";
import { Channel, Guild, GuildMember, Message, User } from ".";
import type { ReactNode } from "react";
import { LiteralUnion } from "type-fest";
@ -24,13 +6,15 @@ import type { FluxEvents } from "./fluxEvents";
export { FluxEvents };
type FluxEventsAutoComplete = LiteralUnion<FluxEvents, string>;
export interface FluxDispatcher {
_actionHandlers: any;
_subscriptions: any;
dispatch(event: { [key: string]: unknown; type: FluxEvents; }): Promise<void>;
dispatch(event: { [key: string]: unknown; type: FluxEventsAutoComplete; }): Promise<void>;
isDispatching(): boolean;
subscribe(event: FluxEvents, callback: (data: any) => void): void;
unsubscribe(event: FluxEvents, callback: (data: any) => void): void;
subscribe(event: FluxEventsAutoComplete, callback: (data: any) => void): void;
unsubscribe(event: FluxEventsAutoComplete, callback: (data: any) => void): void;
wait(callback: () => void): void;
}
@ -335,3 +319,19 @@ export interface DateUtils {
dateFormat(date: Date, format: string): string;
diffAsUnits(start: Date, end: Date, stopAtOneSecond?: boolean): Record<"days" | "hours" | "minutes" | "seconds", number>;
}
export interface CommandOptions {
type: number;
name: string;
description: string;
required?: boolean;
choices?: {
name: string;
values: string | number;
}[];
options?: CommandOptions[];
channel_types?: number[];
min_value?: number;
max_value?: number;
autocomplete?: boolean;
}

View file

@ -1,11 +1,9 @@
/*
* Vencord, a Discord client mod
* @vencord/discord-types
* Copyright (c) 2024 Vendicated, Nuckyz and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-License-Identifier: LGPL-3.0-or-later
*/
import { SYM_ORIGINAL_FACTORY, SYM_PATCHED_BY, SYM_PATCHED_SOURCE } from "./patchWebpack";
export type ModuleExports = any;
export type Module = {
@ -215,24 +213,3 @@ export type WebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & {
/** rspack unique id */
ruid: string;
};
// Utility section for Vencord
export type AnyWebpackRequire = ((moduleId: PropertyKey) => ModuleExports) & Partial<Omit<WebpackRequire, "m">> & {
/** The module factories, where all modules that have been loaded are stored (pre-loaded or loaded by lazy chunks) */
m: Record<PropertyKey, AnyModuleFactory>;
};
/** exports can be anything, however initially it is always an empty object */
export type AnyModuleFactory = ((this: ModuleExports, module: Module, exports: ModuleExports, require: AnyWebpackRequire) => void) & {
[SYM_PATCHED_SOURCE]?: string;
[SYM_PATCHED_BY]?: Set<string>;
};
export type PatchedModuleFactory = AnyModuleFactory & {
[SYM_ORIGINAL_FACTORY]: AnyModuleFactory;
[SYM_PATCHED_SOURCE]?: string;
[SYM_PATCHED_BY]?: Set<string>;
};
export type MaybePatchedModuleFactory = PatchedModuleFactory | AnyModuleFactory;

View file

@ -21,7 +21,6 @@
"@types/node": "^22.13.4",
"@types/react": "18.3.1",
"@types/react-dom": "18.3.1",
"discord-types": "^1.3.26",
"standalone-electron-types": "^34.2.0",
"type-fest": "^4.35.0"
}

43
pnpm-lock.yaml generated
View file

@ -65,12 +65,12 @@ importers:
'@types/yazl':
specifier: ^2.4.5
version: 2.4.6
'@vencord/discord-types':
specifier: link:packages/discord-types
version: link:packages/discord-types
diff:
specifier: ^7.0.0
version: 7.0.0
discord-types:
specifier: ^1.3.26
version: 1.3.26
esbuild:
specifier: ^0.25.1
version: 0.25.1
@ -141,6 +141,18 @@ importers:
specifier: ^0.3.5
version: 0.3.5
packages/discord-types:
dependencies:
'@types/react':
specifier: ^19.0.10
version: 19.0.12
moment:
specifier: ^2.22.2
version: 2.30.1
type-fest:
specifier: ^4.41.0
version: 4.41.0
packages/vencord-types:
dependencies:
'@types/lodash':
@ -155,9 +167,6 @@ importers:
'@types/react-dom':
specifier: 18.3.1
version: 18.3.1
discord-types:
specifier: ^1.3.26
version: 1.3.26
standalone-electron-types:
specifier: ^34.2.0
version: 34.2.0
@ -522,9 +531,6 @@ packages:
peerDependencies:
'@types/react': ^19.0.0
'@types/react@17.0.2':
resolution: {integrity: sha512-Xt40xQsrkdvjn1EyWe1Bc0dJLcil/9x2vAuW7ya+PuQip4UYUaXyhzWmAbwRsdMgwOFHpfp7/FFZebDU6Y8VHA==}
'@types/react@18.3.1':
resolution: {integrity: sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==}
@ -956,9 +962,6 @@ packages:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
discord-types@1.3.26:
resolution: {integrity: sha512-ToG51AOCH+JTQf7b+8vuYQe5Iqwz7nZ7StpECAZ/VZcI1ZhQk13pvt9KkRTfRv1xNvwJ2qib4e3+RifQlo8VPQ==}
doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
@ -2328,6 +2331,10 @@ packages:
resolution: {integrity: sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg==}
engines: {node: '>=16'}
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
typed-array-buffer@1.0.3:
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
engines: {node: '>= 0.4'}
@ -2764,11 +2771,6 @@ snapshots:
dependencies:
'@types/react': 19.0.12
'@types/react@17.0.2':
dependencies:
'@types/prop-types': 15.7.14
csstype: 3.1.3
'@types/react@18.3.1':
dependencies:
'@types/prop-types': 15.7.14
@ -3255,11 +3257,6 @@ snapshots:
dependencies:
path-type: 4.0.0
discord-types@1.3.26:
dependencies:
'@types/react': 17.0.2
moment: 2.30.1
doctrine@2.1.0:
dependencies:
esutils: 2.0.3
@ -4923,6 +4920,8 @@ snapshots:
type-fest@4.38.0: {}
type-fest@4.41.0: {}
typed-array-buffer@1.0.3:
dependencies:
call-bound: 1.0.4

View file

@ -51,7 +51,7 @@ const nodeCommonOpts = {
format: "cjs",
platform: "node",
target: ["esnext"],
// @ts-ignore this is never undefined
// @ts-expect-error this is never undefined
external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external]
};

View file

@ -363,6 +363,6 @@ export const commonRendererPlugins = [
banImportPlugin(/^react$/, "Cannot import from react. React and hooks should be imported from @webpack/common"),
banImportPlugin(/^electron(\/.*)?$/, "Cannot import electron in browser code. You need to use a native.ts file"),
banImportPlugin(/^ts-pattern$/, "Cannot import from ts-pattern. match and P should be imported from @webpack/common"),
// @ts-ignore this is never undefined
// @ts-expect-error this is never undefined
...commonOpts.plugins
];

View file

@ -16,6 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// DO NOT REMOVE UNLESS YOU WISH TO FACE THE WRATH OF THE CIRCULAR DEPENDENCY DEMON!!!!!!!
import "~plugins";
export * as Api from "./api";
export * as Components from "./components";
export * as Plugins from "./plugins";
@ -29,7 +32,8 @@ export { PlainSettings, Settings };
import "./utils/quickCss";
import "./webpack/patchWebpack";
import { openUpdaterModal } from "@components/VencordSettings/UpdaterTab";
import { openUpdaterModal } from "@components/settings/tabs/updater";
import { IS_WINDOWS } from "@utils/constants";
import { StartAt } from "@utils/types";
import { get as dsGet } from "./api/DataStore";
@ -161,7 +165,7 @@ init();
document.addEventListener("DOMContentLoaded", () => {
startAllPlugins(StartAt.DOMContentLoaded);
if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) {
if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && IS_WINDOWS) {
document.head.append(Object.assign(document.createElement("style"), {
id: "vencord-native-titlebar-style",
textContent: "[class*=titleBar]{display: none!important}"

View file

@ -17,10 +17,9 @@
*/
import ErrorBoundary from "@components/ErrorBoundary";
import BadgeAPIPlugin from "plugins/_api/badges";
import { ComponentType, HTMLProps } from "react";
import Plugins from "~plugins";
export const enum BadgePosition {
START,
END
@ -35,7 +34,9 @@ export interface ProfileBadge {
image?: string;
link?: string;
/** Action to perform when you click the badge */
onClick?(event: React.MouseEvent<HTMLButtonElement, MouseEvent>, props: BadgeUserArgs): void;
onClick?(event: React.MouseEvent, props: ProfileBadge & BadgeUserArgs): void;
/** Action to perform when you right click the badge */
onContextMenu?(event: React.MouseEvent, props: BadgeUserArgs & BadgeUserArgs): void;
/** Should the user display this badge? */
shouldShow?(userInfo: BadgeUserArgs): boolean;
/** Optional props (e.g. style) for the badge, ignored for component badges */
@ -77,21 +78,34 @@ export function removeProfileBadge(badge: ProfileBadge) {
export function _getBadges(args: BadgeUserArgs) {
const badges = [] as ProfileBadge[];
for (const badge of Badges) {
if (!badge.shouldShow || badge.shouldShow(args)) {
const b = badge.getBadges
? badge.getBadges(args).map(b => {
b.component &&= ErrorBoundary.wrap(b.component, { noop: true });
return b;
})
: [{ ...badge, ...args }];
if (badge.shouldShow && !badge.shouldShow(args)) {
continue;
}
badge.position === BadgePosition.START
? badges.unshift(...b)
: badges.push(...b);
const b = badge.getBadges
? badge.getBadges(args).map(badge => ({
...args,
...badge,
component: badge.component && ErrorBoundary.wrap(badge.component, { noop: true })
}))
: [{ ...args, ...badge }];
if (badge.position === BadgePosition.START) {
badges.unshift(...b);
} else {
badges.push(...b);
}
}
const donorBadges = (Plugins.BadgeAPI as unknown as typeof import("../plugins/_api/badges").default).getDonorBadges(args.userId);
if (donorBadges) badges.unshift(...donorBadges);
const donorBadges = BadgeAPIPlugin.getDonorBadges(args.userId);
if (donorBadges) {
badges.unshift(
...donorBadges.map(badge => ({
...args,
...badge,
}))
);
}
return badges;
}

View file

@ -8,9 +8,9 @@ import "./ChatButton.css";
import ErrorBoundary from "@components/ErrorBoundary";
import { Logger } from "@utils/Logger";
import { Channel } from "@vencord/discord-types";
import { waitFor } from "@webpack";
import { Button, ButtonWrapperClasses, Tooltip } from "@webpack/common";
import { Channel } from "discord-types/general";
import { HTMLProps, JSX, MouseEventHandler, ReactNode } from "react";
let ChannelTextAreaClasses: Record<"button" | "buttonContainer", string>;

View file

@ -17,13 +17,11 @@
*/
import { mergeDefaults } from "@utils/mergeDefaults";
import { CommandArgument, Message } from "@vencord/discord-types";
import { findByCodeLazy } from "@webpack";
import { MessageActions, SnowflakeUtils } from "@webpack/common";
import { Message } from "discord-types/general";
import type { PartialDeep } from "type-fest";
import { Argument } from "./types";
const createBotMessage = findByCodeLazy('username:"Clyde"');
export function generateId() {
@ -51,8 +49,8 @@ export function sendBotMessage(channelId: string, message: PartialDeep<Message>)
* @param fallbackValue Fallback value in case this option wasn't passed
* @returns Value
*/
export function findOption<T>(args: Argument[], name: string): T & {} | undefined;
export function findOption<T>(args: Argument[], name: string, fallbackValue: T): T & {};
export function findOption(args: Argument[], name: string, fallbackValue?: any) {
export function findOption<T>(args: CommandArgument[], name: string): T & {} | undefined;
export function findOption<T>(args: CommandArgument[], name: string, fallbackValue: T): T & {};
export function findOption(args: CommandArgument[], name: string, fallbackValue?: any) {
return (args.find(a => a.name === name)?.value ?? fallbackValue) as any;
}

View file

@ -18,38 +18,39 @@
import { Logger } from "@utils/Logger";
import { makeCodeblock } from "@utils/text";
import { CommandArgument, CommandContext, CommandOption } from "@vencord/discord-types";
import { sendBotMessage } from "./commandHelpers";
import { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationCommandType, Argument, Command, CommandContext, Option } from "./types";
import { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationCommandType, VencordCommand } from "./types";
export * from "./commandHelpers";
export * from "./types";
export let BUILT_IN: Command[];
export const commands = {} as Record<string, Command>;
export let BUILT_IN: VencordCommand[];
export const commands = {} as Record<string, VencordCommand>;
// hack for plugins being evaluated before we can grab these from webpack
const OptPlaceholder = Symbol("OptionalMessageOption") as any as Option;
const ReqPlaceholder = Symbol("RequiredMessageOption") as any as Option;
const OptPlaceholder = Symbol("OptionalMessageOption") as any as CommandOption;
const ReqPlaceholder = Symbol("RequiredMessageOption") as any as CommandOption;
/**
* Optional message option named "message" you can use in commands.
* Used in "tableflip" or "shrug"
* @see {@link RequiredMessageOption}
*/
export let OptionalMessageOption: Option = OptPlaceholder;
export let OptionalMessageOption: CommandOption = OptPlaceholder;
/**
* Required message option named "message" you can use in commands.
* Used in "me"
* @see {@link OptionalMessageOption}
*/
export let RequiredMessageOption: Option = ReqPlaceholder;
export let RequiredMessageOption: CommandOption = ReqPlaceholder;
// Discord's command list has random gaps for some reason, which can cause issues while rendering the commands
// Add this offset to every added command to keep them unique
let commandIdOffset: number;
export const _init = function (cmds: Command[]) {
export const _init = function (cmds: VencordCommand[]) {
try {
BUILT_IN = cmds;
OptionalMessageOption = cmds.find(c => (c.untranslatedName || c.displayName) === "shrug")!.options![0];
@ -61,7 +62,7 @@ export const _init = function (cmds: Command[]) {
return cmds;
} as never;
export const _handleCommand = function (cmd: Command, args: Argument[], ctx: CommandContext) {
export const _handleCommand = function (cmd: VencordCommand, args: CommandArgument[], ctx: CommandContext) {
if (!cmd.isVencordCommand)
return cmd.execute(args, ctx);
@ -92,7 +93,7 @@ export const _handleCommand = function (cmd: Command, args: Argument[], ctx: Com
* Prepare a Command Option for Discord by filling missing fields
* @param opt
*/
export function prepareOption<O extends Option | Command>(opt: O): O {
export function prepareOption<O extends CommandOption | VencordCommand>(opt: O): O {
opt.displayName ||= opt.name;
opt.displayDescription ||= opt.description;
opt.options?.forEach((opt, i, opts) => {
@ -109,7 +110,7 @@ export function prepareOption<O extends Option | Command>(opt: O): O {
// Yes, Discord registers individual commands for each subcommand
// TODO: This probably doesn't support nested subcommands. If that is ever needed,
// investigate
function registerSubCommands(cmd: Command, plugin: string) {
function registerSubCommands(cmd: VencordCommand, plugin: string) {
cmd.options?.forEach(o => {
if (o.type !== ApplicationCommandOptionType.SUB_COMMAND)
throw new Error("When specifying sub-command options, all options must be sub-commands.");
@ -132,7 +133,7 @@ function registerSubCommands(cmd: Command, plugin: string) {
});
}
export function registerCommand<C extends Command>(command: C, plugin: string) {
export function registerCommand<C extends VencordCommand>(command: C, plugin: string) {
if (!BUILT_IN) {
console.warn(
"[CommandsAPI]",

View file

@ -1,106 +1,12 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Channel, Guild } from "discord-types/general";
import { Promisable } from "type-fest";
import { Command } from "@vencord/discord-types";
export { ApplicationCommandInputType, ApplicationCommandOptionType, ApplicationCommandType } from "@vencord/discord-types/enums";
export interface CommandContext {
channel: Channel;
guild?: Guild;
}
export const enum ApplicationCommandOptionType {
SUB_COMMAND = 1,
SUB_COMMAND_GROUP = 2,
STRING = 3,
INTEGER = 4,
BOOLEAN = 5,
USER = 6,
CHANNEL = 7,
ROLE = 8,
MENTIONABLE = 9,
NUMBER = 10,
ATTACHMENT = 11,
}
export const enum ApplicationCommandInputType {
BUILT_IN = 0,
BUILT_IN_TEXT = 1,
BUILT_IN_INTEGRATION = 2,
BOT = 3,
PLACEHOLDER = 4,
}
export interface Option {
name: string;
displayName?: string;
type: ApplicationCommandOptionType;
description: string;
displayDescription?: string;
required?: boolean;
options?: Option[];
choices?: Array<ChoicesOption>;
}
export interface ChoicesOption {
label: string;
value: string;
name: string;
displayName?: string;
}
export const enum ApplicationCommandType {
CHAT_INPUT = 1,
USER = 2,
MESSAGE = 3,
}
export interface CommandReturnValue {
content: string;
/** TODO: implement */
cancel?: boolean;
}
export interface Argument {
type: ApplicationCommandOptionType;
name: string;
value: string;
focused: undefined;
options: Argument[];
}
export interface Command {
id?: string;
applicationId?: string;
type?: ApplicationCommandType;
inputType?: ApplicationCommandInputType;
plugin?: string;
export interface VencordCommand extends Command {
isVencordCommand?: boolean;
name: string;
untranslatedName?: string;
displayName?: string;
description: string;
untranslatedDescription?: string;
displayDescription?: string;
options?: Option[];
predicate?(ctx: CommandContext): boolean;
execute(args: Argument[], ctx: CommandContext): Promisable<void | CommandReturnValue>;
}

View file

@ -22,9 +22,9 @@ export function promisifyRequest<T = undefined>(
request: IDBRequest<T> | IDBTransaction,
): Promise<T> {
return new Promise<T>((resolve, reject) => {
// @ts-ignore - file size hacks
// @ts-expect-error - file size hacks
request.oncomplete = request.onsuccess = () => resolve(request.result);
// @ts-ignore - file size hacks
// @ts-expect-error - file size hacks
request.onabort = request.onerror = () => reject(request.error);
});
}

View file

@ -17,7 +17,7 @@
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Channel, User } from "discord-types/general/index.js";
import { Channel, User } from "@vencord/discord-types";
import { JSX } from "react";
interface DecoratorProps {

View file

@ -17,7 +17,7 @@
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Channel, Message } from "discord-types/general/index.js";
import { Channel, Message } from "@vencord/discord-types";
import { JSX } from "react";
export interface MessageDecorationProps {

View file

@ -17,9 +17,8 @@
*/
import { Logger } from "@utils/Logger";
import type { Channel, CloudUpload, CustomEmoji, Message } from "@vencord/discord-types";
import { MessageStore } from "@webpack/common";
import { CustomEmoji } from "@webpack/types";
import type { Channel, Message } from "discord-types/general";
import type { Promisable } from "type-fest";
const MessageEventsLogger = new Logger("MessageEvents", "#e5c890");
@ -31,30 +30,6 @@ export interface MessageObject {
tts: boolean;
}
export interface Upload {
classification: string;
currentSize: number;
description: string | null;
filename: string;
id: string;
isImage: boolean;
isVideo: boolean;
item: {
file: File;
platform: number;
};
loaded: number;
mimeType: string;
preCompressionSize: number;
responseUrl: string;
sensitive: boolean;
showLargeMessageDialog: boolean;
spoiler: boolean;
status: "NOT_STARTED" | "STARTED" | "UPLOADING" | "ERROR" | "COMPLETED" | "CANCELLED";
uniqueId: string;
uploadedFilename: string;
}
export interface MessageReplyOptions {
messageReference: Message["messageReference"];
allowedMentions?: {
@ -63,9 +38,9 @@ export interface MessageReplyOptions {
};
}
export interface MessageExtra {
export interface MessageOptions {
stickers?: string[];
uploads?: Upload[];
uploads?: CloudUpload[];
replyOptions: MessageReplyOptions;
content: string;
channel: Channel;
@ -73,17 +48,17 @@ export interface MessageExtra {
openWarningPopout: (props: any) => any;
}
export type MessageSendListener = (channelId: string, messageObj: MessageObject, extra: MessageExtra) => Promisable<void | { cancel: boolean; }>;
export type MessageSendListener = (channelId: string, messageObj: MessageObject, options: MessageOptions) => Promisable<void | { cancel: boolean; }>;
export type MessageEditListener = (channelId: string, messageId: string, messageObj: MessageObject) => Promisable<void | { cancel: boolean; }>;
const sendListeners = new Set<MessageSendListener>();
const editListeners = new Set<MessageEditListener>();
export async function _handlePreSend(channelId: string, messageObj: MessageObject, extra: MessageExtra, replyOptions: MessageReplyOptions) {
extra.replyOptions = replyOptions;
export async function _handlePreSend(channelId: string, messageObj: MessageObject, options: MessageOptions, replyOptions: MessageReplyOptions) {
options.replyOptions = replyOptions;
for (const listener of sendListeners) {
try {
const result = await listener(channelId, messageObj, extra);
const result = await listener(channelId, messageObj, options);
if (result?.cancel) {
return true;
}

View file

@ -18,7 +18,7 @@
import ErrorBoundary from "@components/ErrorBoundary";
import { Logger } from "@utils/Logger";
import { Channel, Message } from "discord-types/general";
import { Channel, Message } from "@vencord/discord-types";
import type { ComponentType, MouseEventHandler } from "react";
const logger = new Logger("MessagePopover");

View file

@ -4,9 +4,8 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Message } from "@vencord/discord-types";
import { MessageCache, MessageStore } from "@webpack/common";
import { FluxStore } from "@webpack/types";
import { Message } from "discord-types/general";
/**
* Update and re-render a message
@ -25,5 +24,5 @@ export function updateMessage(channelId: string, messageId: string, fields?: Par
});
MessageCache.commit(newChannelMessageCache);
(MessageStore as unknown as FluxStore).emitChange();
MessageStore.emitChange();
}

View file

@ -16,7 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { isPrimitiveReactNode } from "@utils/react";
import { waitFor } from "@webpack";
import { ReactNode } from "react";
let NoticesModule: any;
waitFor(m => m.show && m.dismiss && !m.suppressAll, m => NoticesModule = m);
@ -36,7 +39,11 @@ export function nextNotice() {
}
}
export function showNotice(message: string, buttonText: string, onOkClick: () => void) {
noticesQueue.push(["GENERIC", message, buttonText, onOkClick]);
export function showNotice(message: ReactNode, buttonText: string, onOkClick: () => void) {
const notice = isPrimitiveReactNode(message)
? message
: <ErrorBoundary fallback={() => "Error Showing Notice"}>{message}</ErrorBoundary>;
noticesQueue.push(["GENERIC", notice, buttonText, onOkClick]);
if (!currentNotice) nextNotice();
}

View file

@ -104,9 +104,7 @@ export default ErrorBoundary.wrap(function NotificationComponent({
</svg>
</button>
</div>
<div>
{richBody ?? <p className="vc-notification-p">{body}</p>}
</div>
{richBody ?? <p className="vc-notification-p">{body}</p>}
</div>
</div>
{image && <img className="vc-notification-img" src={image} alt="" />}

View file

@ -20,10 +20,10 @@ import * as DataStore from "@api/DataStore";
import { Settings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Flex } from "@components/Flex";
import { openNotificationSettingsModal } from "@components/VencordSettings/NotificationSettings";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { openNotificationSettingsModal } from "@components/settings/tabs/vencord/NotificationSettings";
import { closeModal, ModalCloseButton, ModalFooter, ModalHeader, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { useAwaiter } from "@utils/react";
import { Alerts, Button, Forms, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
import { Alerts, Button, Forms, ListScrollerThin, React, Text, Timestamp, useEffect, useReducer, useState } from "@webpack/common";
import { nanoid } from "nanoid";
import type { DispatchWithoutAction } from "react";
@ -103,21 +103,9 @@ export function useLogs() {
function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
const [removing, setRemoving] = useState(false);
const ref = React.useRef<HTMLDivElement>(null);
useEffect(() => {
const div = ref.current!;
const setHeight = () => {
if (div.clientHeight === 0) return requestAnimationFrame(setHeight);
div.style.height = `${div.clientHeight}px`;
};
setHeight();
}, []);
return (
<div className={cl("wrapper", { removing })} ref={ref}>
<div className={cl("wrapper", { removing })}>
<NotificationComponent
{...data}
permanent={true}
@ -129,13 +117,13 @@ function NotificationEntry({ data }: { data: PersistentNotificationData; }) {
setTimeout(() => deleteNotification(data.timestamp), 200);
}}
richBody={
<div className={cl("body")}>
{data.body}
<div className={cl("body-wrapper")}>
<div className={cl("body")}>{data.body}</div>
<Timestamp timestamp={new Date(data.timestamp)} className={cl("timestamp")} />
</div>
}
/>
</div>
</div >
);
}
@ -151,9 +139,14 @@ export function NotificationLog({ log, pending }: { log: PersistentNotificationD
);
return (
<div className={cl("container")}>
{log.map(n => <NotificationEntry data={n} key={n.id} />)}
</div>
<ListScrollerThin
className={cl("container")}
sections={[log.length]}
sectionHeight={0}
rowHeight={120}
renderSection={() => null}
renderRow={item => <NotificationEntry data={log[item.row]} key={log[item.row].id} />}
/>
);
}
@ -161,15 +154,15 @@ function LogModal({ modalProps, close }: { modalProps: ModalProps; close(): void
const [log, pending] = useLogs();
return (
<ModalRoot {...modalProps} size={ModalSize.LARGE}>
<ModalRoot {...modalProps} size={ModalSize.LARGE} className={cl("modal")}>
<ModalHeader>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Notification Log</Text>
<ModalCloseButton onClick={close} />
</ModalHeader>
<ModalContent>
<div style={{ width: "100%" }}>
<NotificationLog log={log} pending={pending} />
</ModalContent>
</div>
<ModalFooter>
<Flex>

View file

@ -4,17 +4,13 @@
display: flex;
flex-direction: column;
color: var(--text-default);
background-color: var(--background-base-lower-alt);
background-color: var(--background-base-low);
border-radius: 6px;
overflow: hidden;
cursor: pointer;
width: 100%;
}
.visual-refresh .vc-notification-root {
background-color: var(--background-base-low);
}
.vc-notification-root:not(.vc-notification-log-wrapper > .vc-notification-root) {
position: absolute;
z-index: 2147483647;
@ -32,6 +28,7 @@
.vc-notification-content {
width: 100%;
overflow: hidden;
}
.vc-notification-header {
@ -81,6 +78,11 @@
width: 100%;
}
.vc-notification-log-modal {
max-width: 962px;
width: clamp(var(--modal-width-large, 800px), 962px, 85vw);
}
.vc-notification-log-empty {
height: 218px;
background: url("/assets/b36de980b174d7b798c89f35c116e5c6.svg") center no-repeat;
@ -88,19 +90,23 @@
}
.vc-notification-log-container {
display: flex;
flex-direction: column;
padding: 1em;
overflow: hidden;
max-height: min(750px, 75vh);
width: 100%;
}
.vc-notification-log-wrapper {
height: 120px;
width: 100%;
padding-bottom: 16px;
box-sizing: border-box;
transition: 200ms ease;
transition-property: height, opacity;
}
.vc-notification-log-wrapper:not(:last-child) {
margin-bottom: 1em;
/* stylelint-disable-next-line no-descending-specificity */
.vc-notification-root {
height: 104px;
}
}
.vc-notification-log-removing {
@ -109,9 +115,18 @@
margin-bottom: 1em;
}
.vc-notification-log-body {
.vc-notification-log-body-wrapper {
display: flex;
flex-direction: column;
width: 100%;
box-sizing: border-box;
}
.vc-notification-log-body {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
line-height: 1.2em;
}
.vc-notification-log-timestamp {

View file

@ -268,7 +268,7 @@ type ResolveUseSettings<T extends object> = {
[Key in keyof T]:
Key extends string
? T[Key] extends Record<string, unknown>
// @ts-ignore "Type instantiation is excessively deep and possibly infinite"
// @ts-expect-error "Type instantiation is excessively deep and possibly infinite"
? UseSettings<T[Key]> extends string ? `${Key}.${UseSettings<T[Key]>}` : never
: Key
: never;

View file

@ -18,7 +18,6 @@
import { React, TextInput } from "@webpack/common";
// TODO: Refactor settings to use this as well
interface TextInputProps {
/**
* WARNING: Changing this between renders will have no effect!

View file

@ -27,7 +27,6 @@ interface BaseIconProps extends IconProps {
}
type IconProps = JSX.IntrinsicElements["svg"];
type ImageProps = JSX.IntrinsicElements["img"];
function Icon({ height = 24, width = 24, className, children, viewBox, ...svgProps }: PropsWithChildren<BaseIconProps>) {
return (

View file

@ -1,63 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { wordsFromCamel, wordsToTitle } from "@utils/text";
import { PluginOptionBoolean } from "@utils/types";
import { Forms, React, Switch } from "@webpack/common";
import { ISettingElementProps } from ".";
export function SettingBooleanComponent({ option, pluginSettings, definedSettings, id, onChange, onError }: ISettingElementProps<PluginOptionBoolean>) {
const def = pluginSettings[id] ?? option.default;
const [state, setState] = React.useState(def ?? false);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
onError(error !== null);
}, [error]);
function handleChange(newValue: boolean): void {
const isValid = option.isValid?.call(definedSettings, newValue) ?? true;
if (typeof isValid === "string") setError(isValid);
else if (!isValid) setError("Invalid input provided.");
else {
setError(null);
setState(newValue);
onChange(newValue);
}
}
return (
<Forms.FormSection>
<Switch
value={state}
onChange={handleChange}
note={option.description}
disabled={option.disabled?.call(definedSettings) ?? false}
{...option.componentProps}
hideBorder
style={{ marginBottom: "0.5em" }}
>
{wordsToTitle(wordsFromCamel(id))}
</Switch>
{error && <Forms.FormText style={{ color: "var(--text-danger)" }}>{error}</Forms.FormText>}
</Forms.FormSection>
);
}

View file

@ -1,43 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { DefinedSettings, PluginOptionBase } from "@utils/types";
interface ISettingElementPropsBase<T> {
option: T;
onChange(newValue: any): void;
pluginSettings: {
[setting: string]: any;
enabled: boolean;
};
id: string;
onError(hasError: boolean): void;
definedSettings?: DefinedSettings;
}
export type ISettingElementProps<T extends PluginOptionBase> = ISettingElementPropsBase<T>;
export type ISettingCustomElementProps<T extends Omit<PluginOptionBase, "description" | "placeholder">> = ISettingElementPropsBase<T>;
export * from "../../Badge";
export * from "./SettingBooleanComponent";
export * from "./SettingCustomComponent";
export * from "./SettingNumericComponent";
export * from "./SettingSelectComponent";
export * from "./SettingSliderComponent";
export * from "./SettingTextComponent";

View file

@ -1,393 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { CodeBlock } from "@components/CodeBlock";
import { debounce } from "@shared/debounce";
import { copyToClipboard } from "@utils/clipboard";
import { Margins } from "@utils/margins";
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
import { makeCodeblock } from "@utils/text";
import { Patch, ReplaceFn } from "@utils/types";
import { search } from "@webpack";
import { Button, Forms, Parser, React, Switch, TextArea, TextInput } from "@webpack/common";
import { SettingsTab, wrapTab } from "./shared";
// Do not include diff in non dev builds (side effects import)
if (IS_DEV) {
var differ = require("diff") as typeof import("diff");
}
const findCandidates = debounce(function ({ find, setModule, setError }) {
const candidates = search(find);
const keys = Object.keys(candidates);
const len = keys.length;
if (len === 0)
setError("No match. Perhaps that module is lazy loaded?");
else if (len !== 1)
setError("Multiple matches. Please refine your filter");
else
setModule([keys[0], candidates[keys[0]]]);
});
interface ReplacementComponentProps {
module: [id: number, factory: Function];
match: string;
replacement: string | ReplaceFn;
setReplacementError(error: any): void;
}
function ReplacementComponent({ module, match, replacement, setReplacementError }: ReplacementComponentProps) {
const [id, fact] = module;
const [compileResult, setCompileResult] = React.useState<[boolean, string]>();
const [patchedCode, matchResult, diff] = React.useMemo(() => {
const src: string = fact.toString().replaceAll("\n", "");
try {
new RegExp(match);
} catch (e) {
return ["", [], []];
}
const canonicalMatch = canonicalizeMatch(new RegExp(match));
try {
const canonicalReplace = canonicalizeReplace(replacement, 'Vencord.Plugins.plugins["YourPlugin"]');
var patched = src.replace(canonicalMatch, canonicalReplace as string);
setReplacementError(void 0);
} catch (e) {
setReplacementError((e as Error).message);
return ["", [], []];
}
const m = src.match(canonicalMatch);
return [patched, m, makeDiff(src, patched, m)];
}, [id, match, replacement]);
function makeDiff(original: string, patched: string, match: RegExpMatchArray | null) {
if (!match || original === patched) return null;
const changeSize = patched.length - original.length;
// Use 200 surrounding characters of context
const start = Math.max(0, match.index! - 200);
const end = Math.min(original.length, match.index! + match[0].length + 200);
// (changeSize may be negative)
const endPatched = end + changeSize;
const context = original.slice(start, end);
const patchedContext = patched.slice(start, endPatched);
return differ.diffWordsWithSpace(context, patchedContext);
}
function renderMatch() {
if (!matchResult)
return <Forms.FormText>Regex doesn't match!</Forms.FormText>;
const fullMatch = matchResult[0] ? makeCodeblock(matchResult[0], "js") : "";
const groups = matchResult.length > 1
? makeCodeblock(matchResult.slice(1).map((g, i) => `Group ${i + 1}: ${g}`).join("\n"), "yml")
: "";
return (
<>
<div style={{ userSelect: "text" }}>{Parser.parse(fullMatch)}</div>
<div style={{ userSelect: "text" }}>{Parser.parse(groups)}</div>
</>
);
}
function renderDiff() {
return diff?.map((p, idx) => {
const color = p.added ? "lime" : p.removed ? "red" : "grey";
return <div key={idx} style={{ color, userSelect: "text", wordBreak: "break-all", lineBreak: "anywhere" }}>{p.value}</div>;
});
}
return (
<>
<Forms.FormTitle>Module {id}</Forms.FormTitle>
{!!matchResult?.[0]?.length && (
<>
<Forms.FormTitle>Match</Forms.FormTitle>
{renderMatch()}
</>)
}
{!!diff?.length && (
<>
<Forms.FormTitle>Diff</Forms.FormTitle>
{renderDiff()}
</>
)}
{!!diff?.length && (
<Button className={Margins.top20} onClick={() => {
try {
Function(patchedCode.replace(/^function\(/, "function patchedModule("));
setCompileResult([true, "Compiled successfully"]);
} catch (err) {
setCompileResult([false, (err as Error).message]);
}
}}>Compile</Button>
)}
{compileResult &&
<Forms.FormText style={{ color: compileResult[0] ? "var(--status-positive)" : "var(--text-danger)" }}>
{compileResult[1]}
</Forms.FormText>
}
</>
);
}
function ReplacementInput({ replacement, setReplacement, replacementError }) {
const [isFunc, setIsFunc] = React.useState(false);
const [error, setError] = React.useState<string>();
function onChange(v: string) {
setError(void 0);
if (isFunc) {
try {
const func = (0, eval)(v);
if (typeof func === "function")
setReplacement(() => func);
else
setError("Replacement must be a function");
} catch (e) {
setReplacement(v);
setError((e as Error).message);
}
} else {
setReplacement(v);
}
}
React.useEffect(
() => void (isFunc ? onChange(replacement) : setError(void 0)),
[isFunc]
);
return (
<>
{/* FormTitle adds a class if className is not set, so we set it to an empty string to prevent that */}
<Forms.FormTitle className="">replacement</Forms.FormTitle>
<TextInput
value={replacement?.toString()}
onChange={onChange}
error={error ?? replacementError}
/>
{!isFunc && (
<div className="vc-text-selectable">
<Forms.FormTitle className={Margins.top8}>Cheat Sheet</Forms.FormTitle>
{Object.entries({
"\\i": "Special regex escape sequence that matches identifiers (varnames, classnames, etc.)",
"$$": "Insert a $",
"$&": "Insert the entire match",
"$`\u200b": "Insert the substring before the match",
"$'": "Insert the substring after the match",
"$n": "Insert the nth capturing group ($1, $2...)",
"$self": "Insert the plugin instance",
}).map(([placeholder, desc]) => (
<Forms.FormText key={placeholder}>
{Parser.parse("`" + placeholder + "`")}: {desc}
</Forms.FormText>
))}
</div>
)}
<Switch
className={Margins.top8}
value={isFunc}
onChange={setIsFunc}
note="'replacement' will be evaled if this is toggled"
hideBorder={true}
>
Treat as Function
</Switch>
</>
);
}
interface FullPatchInputProps {
setFind(v: string): void;
setParsedFind(v: string | RegExp): void;
setMatch(v: string): void;
setReplacement(v: string | ReplaceFn): void;
}
function FullPatchInput({ setFind, setParsedFind, setMatch, setReplacement }: FullPatchInputProps) {
const [fullPatch, setFullPatch] = React.useState<string>("");
const [fullPatchError, setFullPatchError] = React.useState<string>("");
function update() {
if (fullPatch === "") {
setFullPatchError("");
setFind("");
setParsedFind("");
setMatch("");
setReplacement("");
return;
}
try {
const parsed = (0, eval)(`([${fullPatch}][0])`) as Patch;
if (!parsed.find) throw new Error("No 'find' field");
if (!parsed.replacement) throw new Error("No 'replacement' field");
if (parsed.replacement instanceof Array) {
if (parsed.replacement.length === 0) throw new Error("Invalid replacement");
parsed.replacement = {
match: parsed.replacement[0].match,
replace: parsed.replacement[0].replace
};
}
if (!parsed.replacement.match) throw new Error("No 'replacement.match' field");
if (!parsed.replacement.replace) throw new Error("No 'replacement.replace' field");
setFind(parsed.find instanceof RegExp ? parsed.find.toString() : parsed.find);
setParsedFind(parsed.find);
setMatch(parsed.replacement.match instanceof RegExp ? parsed.replacement.match.source : parsed.replacement.match);
setReplacement(parsed.replacement.replace);
setFullPatchError("");
} catch (e) {
setFullPatchError((e as Error).message);
}
}
return <>
<Forms.FormText className={Margins.bottom8}>Paste your full JSON patch here to fill out the fields</Forms.FormText>
<TextArea value={fullPatch} onChange={setFullPatch} onBlur={update} />
{fullPatchError !== "" && <Forms.FormText style={{ color: "var(--text-danger)" }}>{fullPatchError}</Forms.FormText>}
</>;
}
function PatchHelper() {
const [find, setFind] = React.useState<string>("");
const [parsedFind, setParsedFind] = React.useState<string | RegExp>("");
const [match, setMatch] = React.useState<string>("");
const [replacement, setReplacement] = React.useState<string | ReplaceFn>("");
const [replacementError, setReplacementError] = React.useState<string>();
const [module, setModule] = React.useState<[number, Function]>();
const [findError, setFindError] = React.useState<string>();
const [matchError, setMatchError] = React.useState<string>();
const code = React.useMemo(() => {
return `
{
find: ${parsedFind instanceof RegExp ? parsedFind.toString() : JSON.stringify(parsedFind)},
replacement: {
match: /${match.replace(/(?<!\\)\//g, "\\/")}/,
replace: ${typeof replacement === "function" ? replacement.toString() : JSON.stringify(replacement)}
}
}
`.trim();
}, [parsedFind, match, replacement]);
function onFindChange(v: string) {
setFind(v);
try {
let parsedFind = v as string | RegExp;
if (/^\/.+?\/$/.test(v)) parsedFind = new RegExp(v.slice(1, -1));
setFindError(void 0);
setParsedFind(parsedFind);
if (v.length) {
findCandidates({ find: parsedFind, setModule, setError: setFindError });
}
} catch (e: any) {
setFindError((e as Error).message);
}
}
function onMatchChange(v: string) {
setMatch(v);
try {
new RegExp(v);
setMatchError(void 0);
} catch (e: any) {
setMatchError((e as Error).message);
}
}
return (
<SettingsTab title="Patch Helper">
<Forms.FormTitle>full patch</Forms.FormTitle>
<FullPatchInput
setFind={onFindChange}
setParsedFind={setParsedFind}
setMatch={onMatchChange}
setReplacement={setReplacement}
/>
<Forms.FormTitle className={Margins.top8}>find</Forms.FormTitle>
<TextInput
type="text"
value={find}
onChange={onFindChange}
error={findError}
/>
<Forms.FormTitle className={Margins.top8}>match</Forms.FormTitle>
<TextInput
type="text"
value={match}
onChange={onMatchChange}
error={matchError}
/>
<div className={Margins.top8} />
<ReplacementInput
replacement={replacement}
setReplacement={setReplacement}
replacementError={replacementError}
/>
<Forms.FormDivider />
{module && (
<ReplacementComponent
module={module}
match={match}
replacement={replacement}
setReplacementError={setReplacementError}
/>
)}
{!!(find && match && replacement) && (
<>
<Forms.FormTitle className={Margins.top20}>Code</Forms.FormTitle>
<CodeBlock lang="js" content={code} />
<Button onClick={() => copyToClipboard(code)}>Copy to Clipboard</Button>
<Button className={Margins.top8} onClick={() => copyToClipboard("```ts\n" + code + "\n```")}>Copy as Codeblock</Button>
</>
)}
</SettingsTab>
);
}
export default IS_DEV ? wrapTab(PatchHelper, "PatchHelper") : null;

View file

@ -1,23 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import PluginSettings from "@components/PluginSettings";
import { wrapTab } from "./shared";
export default wrapTab(PluginSettings, "Plugins");

View file

@ -1,453 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Settings, useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { DeleteIcon, FolderIcon, PaintbrushIcon, PencilIcon, PlusIcon, RestartIcon } from "@components/Icons";
import { Link } from "@components/Link";
import { openPluginModal } from "@components/PluginSettings/PluginModal";
import type { UserThemeHeader } from "@main/themes";
import { CspBlockedUrls, useCspErrors } from "@utils/cspViolations";
import { openInviteModal } from "@utils/discord";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { relaunch } from "@utils/native";
import { useAwaiter, useForceUpdater } from "@utils/react";
import { getStylusWebStoreUrl } from "@utils/web";
import { findLazy } from "@webpack";
import { Alerts, Button, Card, Forms, React, showToast, TabBar, TextArea, useEffect, useRef, useState } from "@webpack/common";
import type { ComponentType, Ref, SyntheticEvent } from "react";
import Plugins from "~plugins";
import { AddonCard } from "./AddonCard";
import { QuickAction, QuickActionCard } from "./quickActions";
import { SettingsTab, wrapTab } from "./shared";
type FileInput = ComponentType<{
ref: Ref<HTMLInputElement>;
onChange: (e: SyntheticEvent<HTMLInputElement>) => void;
multiple?: boolean;
filters?: { name?: string; extensions: string[]; }[];
}>;
const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef);
const cl = classNameFactory("vc-settings-theme-");
function Validator({ link }: { link: string; }) {
const [res, err, pending] = useAwaiter(() => fetch(link).then(res => {
if (res.status > 300) throw `${res.status} ${res.statusText}`;
const contentType = res.headers.get("Content-Type");
if (!contentType?.startsWith("text/css") && !contentType?.startsWith("text/plain"))
throw "Not a CSS file. Remember to use the raw link!";
return "Okay!";
}));
const text = pending
? "Checking..."
: err
? `Error: ${err instanceof Error ? err.message : String(err)}`
: "Valid!";
return <Forms.FormText style={{
color: pending ? "var(--text-muted)" : err ? "var(--text-danger)" : "var(--status-positive)"
}}>{text}</Forms.FormText>;
}
function Validators({ themeLinks }: { themeLinks: string[]; }) {
if (!themeLinks.length) return null;
return (
<>
<Forms.FormTitle className={Margins.top20} tag="h5">Validator</Forms.FormTitle>
<Forms.FormText>This section will tell you whether your themes can successfully be loaded</Forms.FormText>
<div>
{themeLinks.map(rawLink => {
const { label, link } = (() => {
const match = /^@(light|dark) (.*)/.exec(rawLink);
if (!match) return { label: rawLink, link: rawLink };
const [, mode, link] = match;
return { label: `[${mode} mode only] ${link}`, link };
})();
return <Card style={{
padding: ".5em",
marginBottom: ".5em",
marginTop: ".5em"
}} key={link}>
<Forms.FormTitle tag="h5" style={{
overflowWrap: "break-word"
}}>
{label}
</Forms.FormTitle>
<Validator link={link} />
</Card>;
})}
</div>
</>
);
}
interface ThemeCardProps {
theme: UserThemeHeader;
enabled: boolean;
onChange: (enabled: boolean) => void;
onDelete: () => void;
}
function ThemeCard({ theme, enabled, onChange, onDelete }: ThemeCardProps) {
return (
<AddonCard
name={theme.name}
description={theme.description}
author={theme.author}
enabled={enabled}
setEnabled={onChange}
infoButton={
IS_WEB && (
<div style={{ cursor: "pointer", color: "var(--status-danger" }} onClick={onDelete}>
<DeleteIcon />
</div>
)
}
footer={
<Flex flexDirection="row" style={{ gap: "0.2em" }}>
{!!theme.website && <Link href={theme.website}>Website</Link>}
{!!(theme.website && theme.invite) && " • "}
{!!theme.invite && (
<Link
href={`https://discord.gg/${theme.invite}`}
onClick={async e => {
e.preventDefault();
theme.invite != null && openInviteModal(theme.invite).catch(() => showToast("Invalid or expired invite"));
}}
>
Discord Server
</Link>
)}
</Flex>
}
/>
);
}
enum ThemeTab {
LOCAL,
ONLINE
}
function ThemesTab() {
const settings = useSettings(["themeLinks", "enabledThemes"]);
const fileInputRef = useRef<HTMLInputElement>(null);
const [currentTab, setCurrentTab] = useState(ThemeTab.LOCAL);
const [themeText, setThemeText] = useState(settings.themeLinks.join("\n"));
const [userThemes, setUserThemes] = useState<UserThemeHeader[] | null>(null);
useEffect(() => {
refreshLocalThemes();
}, []);
async function refreshLocalThemes() {
const themes = await VencordNative.themes.getThemesList();
setUserThemes(themes);
}
// When a local theme is enabled/disabled, update the settings
function onLocalThemeChange(fileName: string, value: boolean) {
if (value) {
if (settings.enabledThemes.includes(fileName)) return;
settings.enabledThemes = [...settings.enabledThemes, fileName];
} else {
settings.enabledThemes = settings.enabledThemes.filter(f => f !== fileName);
}
}
async function onFileUpload(e: SyntheticEvent<HTMLInputElement>) {
e.stopPropagation();
e.preventDefault();
if (!e.currentTarget?.files?.length) return;
const { files } = e.currentTarget;
const uploads = Array.from(files, file => {
const { name } = file;
if (!name.endsWith(".css")) return;
return new Promise<void>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
VencordNative.themes.uploadTheme(name, reader.result as string)
.then(resolve)
.catch(reject);
};
reader.readAsText(file);
});
});
await Promise.all(uploads);
refreshLocalThemes();
}
function renderLocalThemes() {
return (
<>
<Card className="vc-settings-card">
<Forms.FormTitle tag="h5">Find Themes:</Forms.FormTitle>
<div style={{ marginBottom: ".5em", display: "flex", flexDirection: "column" }}>
<Link style={{ marginRight: ".5em" }} href="https://betterdiscord.app/themes">
BetterDiscord Themes
</Link>
<Link href="https://github.com/search?q=discord+theme">GitHub</Link>
</div>
<Forms.FormText>If using the BD site, click on "Download" and place the downloaded .theme.css file into your themes folder.</Forms.FormText>
</Card>
<Card className="vc-settings-card">
<Forms.FormTitle tag="h5">External Resources</Forms.FormTitle>
<Forms.FormText>For security reasons, loading resources (styles, fonts, images, ...) from most sites is blocked.</Forms.FormText>
<Forms.FormText>Make sure all your assets are hosted on GitHub, GitLab, Codeberg, Imgur, Discord or Google Fonts.</Forms.FormText>
</Card>
<Forms.FormSection title="Local Themes">
<QuickActionCard>
<>
{IS_WEB ?
(
<QuickAction
text={
<span style={{ position: "relative" }}>
Upload Theme
<FileInput
ref={fileInputRef}
onChange={onFileUpload}
multiple={true}
filters={[{ extensions: ["css"] }]}
/>
</span>
}
Icon={PlusIcon}
/>
) : (
<QuickAction
text="Open Themes Folder"
action={() => VencordNative.themes.openFolder()}
Icon={FolderIcon}
/>
)}
<QuickAction
text="Load missing Themes"
action={refreshLocalThemes}
Icon={RestartIcon}
/>
<QuickAction
text="Edit QuickCSS"
action={() => VencordNative.quickCss.openEditor()}
Icon={PaintbrushIcon}
/>
{Settings.plugins.ClientTheme.enabled && (
<QuickAction
text="Edit ClientTheme"
action={() => openPluginModal(Plugins.ClientTheme)}
Icon={PencilIcon}
/>
)}
</>
</QuickActionCard>
<div className={cl("grid")}>
{userThemes?.map(theme => (
<ThemeCard
key={theme.fileName}
enabled={settings.enabledThemes.includes(theme.fileName)}
onChange={enabled => onLocalThemeChange(theme.fileName, enabled)}
onDelete={async () => {
onLocalThemeChange(theme.fileName, false);
await VencordNative.themes.deleteTheme(theme.fileName);
refreshLocalThemes();
}}
theme={theme}
/>
))}
</div>
</Forms.FormSection>
</>
);
}
// When the user leaves the online theme textbox, update the settings
function onBlur() {
settings.themeLinks = [...new Set(
themeText
.trim()
.split(/\n+/)
.map(s => s.trim())
.filter(Boolean)
)];
}
function renderOnlineThemes() {
return (
<>
<Card className="vc-settings-card vc-text-selectable">
<Forms.FormTitle tag="h5">Paste links to css files here</Forms.FormTitle>
<Forms.FormText>One link per line</Forms.FormText>
<Forms.FormText>You can prefix lines with @light or @dark to toggle them based on your Discord theme</Forms.FormText>
<Forms.FormText>Make sure to use direct links to files (raw or github.io)!</Forms.FormText>
</Card>
<Forms.FormSection title="Online Themes" tag="h5">
<TextArea
value={themeText}
onChange={setThemeText}
className={"vc-settings-theme-links"}
placeholder="Theme Links"
spellCheck={false}
onBlur={onBlur}
rows={10}
/>
<Validators themeLinks={settings.themeLinks} />
</Forms.FormSection>
</>
);
}
return (
<SettingsTab title="Themes">
<TabBar
type="top"
look="brand"
className="vc-settings-tab-bar"
selectedItem={currentTab}
onItemSelect={setCurrentTab}
>
<TabBar.Item
className="vc-settings-tab-bar-item"
id={ThemeTab.LOCAL}
>
Local Themes
</TabBar.Item>
<TabBar.Item
className="vc-settings-tab-bar-item"
id={ThemeTab.ONLINE}
>
Online Themes
</TabBar.Item>
</TabBar>
<CspErrorCard />
{currentTab === ThemeTab.LOCAL && renderLocalThemes()}
{currentTab === ThemeTab.ONLINE && renderOnlineThemes()}
</SettingsTab>
);
}
export function CspErrorCard() {
if (IS_WEB) return null;
const errors = useCspErrors();
const forceUpdate = useForceUpdater();
if (!errors.length) return null;
const isImgurHtmlDomain = (url: string) => url.startsWith("https://imgur.com/");
const allowUrl = async (url: string) => {
const { origin: baseUrl, host } = new URL(url);
const result = await VencordNative.csp.requestAddOverride(baseUrl, ["connect-src", "img-src", "style-src", "font-src"], "Vencord Themes");
if (result !== "ok") return;
CspBlockedUrls.forEach(url => {
if (new URL(url).host === host) {
CspBlockedUrls.delete(url);
}
});
forceUpdate();
Alerts.show({
title: "Restart Required",
body: "A restart is required to apply this change",
confirmText: "Restart now",
cancelText: "Later!",
onConfirm: relaunch
});
};
const hasImgurHtmlDomain = errors.some(isImgurHtmlDomain);
return (
<ErrorCard className="vc-settings-card">
<Forms.FormTitle tag="h5">Blocked Resources</Forms.FormTitle>
<Forms.FormText>Some images, styles, or fonts were blocked because they come from disallowed domains.</Forms.FormText>
<Forms.FormText>It is highly recommended to move them to GitHub or Imgur. But you may also allow domains if you fully trust them.</Forms.FormText>
<Forms.FormText>
After allowing a domain, you have to fully close (from tray / task manager) and restart {IS_DISCORD_DESKTOP ? "Discord" : "Vesktop"} to apply the change.
</Forms.FormText>
<Forms.FormTitle tag="h5" className={classes(Margins.top16, Margins.bottom8)}>Blocked URLs</Forms.FormTitle>
<div className="vc-settings-csp-list">
{errors.map((url, i) => (
<div key={url}>
{i !== 0 && <Forms.FormDivider className={Margins.bottom8} />}
<div className="vc-settings-csp-row">
<Link href={url}>{url}</Link>
<Button color={Button.Colors.PRIMARY} onClick={() => allowUrl(url)} disabled={isImgurHtmlDomain(url)}>
Allow
</Button>
</div>
</div>
))}
</div>
{hasImgurHtmlDomain && (
<>
<Forms.FormDivider className={classes(Margins.top8, Margins.bottom16)} />
<Forms.FormText>
Imgur links should be direct links in the form of <code>https://i.imgur.com/...</code>
</Forms.FormText>
<Forms.FormText>To obtain a direct link, right-click the image and select "Copy image address".</Forms.FormText>
</>
)}
</ErrorCard>
);
}
function UserscriptThemesTab() {
return (
<SettingsTab title="Themes">
<Card className="vc-settings-card">
<Forms.FormTitle tag="h5">Themes are not supported on the Userscript!</Forms.FormTitle>
<Forms.FormText>
You can instead install themes with the <Link href={getStylusWebStoreUrl()}>Stylus extension</Link>!
</Forms.FormText>
</Card>
</SettingsTab>
);
}
export default IS_USERSCRIPT
? wrapTab(UserscriptThemesTab, "Themes")
: wrapTab(ThemesTab, "Themes");

View file

@ -1,268 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { useSettings } from "@api/Settings";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
import { Link } from "@components/Link";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { ModalCloseButton, ModalContent, ModalProps, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { relaunch } from "@utils/native";
import { useAwaiter } from "@utils/react";
import { changes, checkForUpdates, getRepo, isNewer, update, updateError, UpdateLogger } from "@utils/updater";
import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@webpack/common";
import gitHash from "~git-hash";
import { handleSettingsTabError, SettingsTab, wrapTab } from "./shared";
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
return async () => {
dispatcher(true);
try {
await action();
} catch (e: any) {
UpdateLogger.error("Failed to update", e);
let err: string;
if (!e) {
err = "An unknown error occurred (error is undefined).\nPlease try again.";
} else if (e.code && e.cmd) {
const { code, path, cmd, stderr } = e;
if (code === "ENOENT")
err = `Command \`${path}\` not found.\nPlease install it and try again`;
else {
err = `An error occurred while running \`${cmd}\`:\n`;
err += stderr || `Code \`${code}\`. See the console for more info`;
}
} else {
err = "An unknown error occurred. See the console for more info.";
}
Alerts.show({
title: "Oops!",
body: (
<ErrorCard>
{err.split("\n").map((line, idx) => <div key={idx}>{Parser.parse(line)}</div>)}
</ErrorCard>
)
});
}
finally {
dispatcher(false);
}
};
}
interface CommonProps {
repo: string;
repoPending: boolean;
}
function HashLink({ repo, hash, disabled = false }: { repo: string, hash: string, disabled?: boolean; }) {
return <Link href={`${repo}/commit/${hash}`} disabled={disabled}>
{hash}
</Link>;
}
function Changes({ updates, repo, repoPending }: CommonProps & { updates: typeof changes; }) {
return (
<Card style={{ padding: "0 0.5em" }}>
{updates.map(({ hash, author, message }) => (
<div key={hash} style={{
marginTop: "0.5em",
marginBottom: "0.5em"
}}>
<code><HashLink {...{ repo, hash }} disabled={repoPending} /></code>
<span style={{
marginLeft: "0.5em",
color: "var(--text-default)"
}}>{message} - {author}</span>
</div>
))}
</Card>
);
}
function Updatable(props: CommonProps) {
const [updates, setUpdates] = React.useState(changes);
const [isChecking, setIsChecking] = React.useState(false);
const [isUpdating, setIsUpdating] = React.useState(false);
const isOutdated = (updates?.length ?? 0) > 0;
return (
<>
{!updates && updateError ? (
<>
<Forms.FormText>Failed to check updates. Check the console for more info</Forms.FormText>
<ErrorCard style={{ padding: "1em" }}>
<p>{updateError.stderr || updateError.stdout || "An unknown error occurred"}</p>
</ErrorCard>
</>
) : (
<Forms.FormText className={Margins.bottom8}>
{isOutdated ? (updates.length === 1 ? "There is 1 Update" : `There are ${updates.length} Updates`) : "Up to Date!"}
</Forms.FormText>
)}
{isOutdated && <Changes updates={updates} {...props} />}
<Flex className={classes(Margins.bottom8, Margins.top8)}>
{isOutdated && <Button
size={Button.Sizes.SMALL}
disabled={isUpdating || isChecking}
onClick={withDispatcher(setIsUpdating, async () => {
if (await update()) {
setUpdates([]);
await new Promise<void>(r => {
Alerts.show({
title: "Update Success!",
body: "Successfully updated. Restart now to apply the changes?",
confirmText: "Restart",
cancelText: "Not now!",
onConfirm() {
relaunch();
r();
},
onCancel: r
});
});
}
})}
>
Update Now
</Button>}
<Button
size={Button.Sizes.SMALL}
disabled={isUpdating || isChecking}
onClick={withDispatcher(setIsChecking, async () => {
const outdated = await checkForUpdates();
if (outdated) {
setUpdates(changes);
} else {
setUpdates([]);
Toasts.show({
message: "No updates found!",
id: Toasts.genId(),
type: Toasts.Type.MESSAGE,
options: {
position: Toasts.Position.BOTTOM
}
});
}
})}
>
Check for Updates
</Button>
</Flex>
</>
);
}
function Newer(props: CommonProps) {
return (
<>
<Forms.FormText className={Margins.bottom8}>
Your local copy has more recent commits. Please stash or reset them.
</Forms.FormText>
<Changes {...props} updates={changes} />
</>
);
}
function Updater() {
const settings = useSettings(["autoUpdate", "autoUpdateNotification"]);
const [repo, err, repoPending] = useAwaiter(getRepo, { fallbackValue: "Loading..." });
React.useEffect(() => {
if (err)
UpdateLogger.error("Failed to retrieve repo", err);
}, [err]);
const commonProps: CommonProps = {
repo,
repoPending
};
return (
<SettingsTab title="Vencord Updater">
<Forms.FormTitle tag="h5">Updater Settings</Forms.FormTitle>
<Switch
value={settings.autoUpdate}
onChange={(v: boolean) => settings.autoUpdate = v}
note="Automatically update Vencord without confirmation prompt"
>
Automatically update
</Switch>
<Switch
value={settings.autoUpdateNotification}
onChange={(v: boolean) => settings.autoUpdateNotification = v}
note="Shows a notification when Vencord automatically updates"
disabled={!settings.autoUpdate}
>
Get notified when an automatic update completes
</Switch>
<Forms.FormTitle tag="h5">Repo</Forms.FormTitle>
<Forms.FormText className="vc-text-selectable">
{repoPending
? repo
: err
? "Failed to retrieve - check console"
: (
<Link href={repo}>
{repo.split("/").slice(-2).join("/")}
</Link>
)
}
{" "}(<HashLink hash={gitHash} repo={repo} disabled={repoPending} />)
</Forms.FormText>
<Forms.FormDivider className={Margins.top8 + " " + Margins.bottom8} />
<Forms.FormTitle tag="h5">Updates</Forms.FormTitle>
{isNewer ? <Newer {...commonProps} /> : <Updatable {...commonProps} />}
</SettingsTab>
);
}
export default IS_UPDATER_DISABLED ? null : wrapTab(Updater, "Updater");
export const openUpdaterModal = IS_UPDATER_DISABLED ? null : function () {
const UpdaterTab = wrapTab(Updater, "Updater");
try {
openModal(wrapTab((modalProps: ModalProps) => (
<ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
<ModalContent className="vc-updater-modal">
<ModalCloseButton onClick={modalProps.onClose} className="vc-updater-modal-close-button" />
<UpdaterTab />
</ModalContent>
</ModalRoot>
), "UpdaterModal"));
} catch {
handleSettingsTabError();
}
};

View file

@ -1,301 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { openNotificationLogModal } from "@api/Notifications/notificationLog";
import { useSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import DonateButton from "@components/DonateButton";
import { openContributorModal } from "@components/PluginSettings/ContributorModal";
import { openPluginModal } from "@components/PluginSettings/PluginModal";
import { gitRemote } from "@shared/vencordUserAgent";
import { DONOR_ROLE_ID, VENCORD_GUILD_ID } from "@utils/constants";
import { Margins } from "@utils/margins";
import { identity, isPluginDev } from "@utils/misc";
import { relaunch } from "@utils/native";
import { Button, Forms, GuildMemberStore, React, Select, Switch, UserStore } from "@webpack/common";
import BadgeAPI from "../../plugins/_api/badges";
import { Flex, FolderIcon, GithubIcon, LogIcon, PaintbrushIcon, RestartIcon } from "..";
import { openNotificationSettingsModal } from "./NotificationSettings";
import { QuickAction, QuickActionCard } from "./quickActions";
import { SettingsTab, wrapTab } from "./shared";
import { SpecialCard } from "./SpecialCard";
const cl = classNameFactory("vc-settings-");
const DEFAULT_DONATE_IMAGE = "https://cdn.discordapp.com/emojis/1026533090627174460.png";
const SHIGGY_DONATE_IMAGE = "https://media.discordapp.net/stickers/1039992459209490513.png";
const VENNIE_DONATOR_IMAGE = "https://cdn.discordapp.com/emojis/1238120638020063377.png";
const COZY_CONTRIB_IMAGE = "https://cdn.discordapp.com/emojis/1026533070955872337.png";
const DONOR_BACKGROUND_IMAGE = "https://media.discordapp.net/stickers/1311070116305436712.png?size=2048";
const CONTRIB_BACKGROUND_IMAGE = "https://media.discordapp.net/stickers/1311070166481895484.png?size=2048";
type KeysOfType<Object, Type> = {
[K in keyof Object]: Object[K] extends Type ? K : never;
}[keyof Object];
function VencordSettings() {
const settings = useSettings();
const donateImage = React.useMemo(() => Math.random() > 0.5 ? DEFAULT_DONATE_IMAGE : SHIGGY_DONATE_IMAGE, []);
const isWindows = navigator.platform.toLowerCase().startsWith("win");
const isMac = navigator.platform.toLowerCase().startsWith("mac");
const needsVibrancySettings = IS_DISCORD_DESKTOP && isMac;
const user = UserStore.getCurrentUser();
const Switches: Array<false | {
key: KeysOfType<typeof settings, boolean>;
title: string;
note: string;
}> =
[
{
key: "useQuickCss",
title: "Enable Custom CSS",
note: "Loads your Custom CSS"
},
!IS_WEB && {
key: "enableReactDevtools",
title: "Enable React Developer Tools",
note: "Requires a full restart"
},
!IS_WEB && (!IS_DISCORD_DESKTOP || !isWindows ? {
key: "frameless",
title: "Disable the window frame",
note: "Requires a full restart"
} : {
key: "winNativeTitleBar",
title: "Use Windows' native title bar instead of Discord's custom one",
note: "Requires a full restart"
}),
!IS_WEB && {
key: "transparent",
title: "Enable window transparency.",
note: "You need a theme that supports transparency or this will do nothing. WILL STOP THE WINDOW FROM BEING RESIZABLE!! Requires a full restart"
},
!IS_WEB && isWindows && {
key: "winCtrlQ",
title: "Register Ctrl+Q as shortcut to close Discord (Alternative to Alt+F4)",
note: "Requires a full restart"
},
IS_DISCORD_DESKTOP && {
key: "disableMinSize",
title: "Disable minimum window size",
note: "Requires a full restart"
},
];
return (
<SettingsTab title="Vencord Settings">
{isDonor(user?.id)
? (
<SpecialCard
title="Donations"
subtitle="Thank you for donating!"
description="You can manage your perks at any time by messaging @vending.machine."
cardImage={VENNIE_DONATOR_IMAGE}
backgroundImage={DONOR_BACKGROUND_IMAGE}
backgroundColor="#ED87A9"
>
<DonateButtonComponent />
</SpecialCard>
)
: (
<SpecialCard
title="Support the Project"
description="Please consider supporting the development of Vencord by donating!"
cardImage={donateImage}
backgroundImage={DONOR_BACKGROUND_IMAGE}
backgroundColor="#c3a3ce"
>
<DonateButtonComponent />
</SpecialCard>
)
}
{isPluginDev(user?.id) && (
<SpecialCard
title="Contributions"
subtitle="Thank you for contributing!"
description="Since you've contributed to Vencord you now have a cool new badge!"
cardImage={COZY_CONTRIB_IMAGE}
backgroundImage={CONTRIB_BACKGROUND_IMAGE}
backgroundColor="#EDCC87"
buttonTitle="See what you've contributed to"
buttonOnClick={() => openContributorModal(user)}
/>
)}
<Forms.FormSection title="Quick Actions">
<QuickActionCard>
<QuickAction
Icon={LogIcon}
text="Notification Log"
action={openNotificationLogModal}
/>
<QuickAction
Icon={PaintbrushIcon}
text="Edit QuickCSS"
action={() => VencordNative.quickCss.openEditor()}
/>
{!IS_WEB && (
<QuickAction
Icon={RestartIcon}
text="Relaunch Discord"
action={relaunch}
/>
)}
{!IS_WEB && (
<QuickAction
Icon={FolderIcon}
text="Open Settings Folder"
action={() => VencordNative.settings.openFolder()}
/>
)}
<QuickAction
Icon={GithubIcon}
text="View Source Code"
action={() => VencordNative.native.openExternal("https://github.com/" + gitRemote)}
/>
</QuickActionCard>
</Forms.FormSection>
<Forms.FormDivider />
<Forms.FormSection className={Margins.top16} title="Settings" tag="h5">
<Forms.FormText className={Margins.bottom20} style={{ color: "var(--text-muted)" }}>
Hint: You can change the position of this settings section in the
{" "}<Button
look={Button.Looks.BLANK}
style={{ color: "var(--text-link)", display: "inline-block" }}
onClick={() => openPluginModal(Vencord.Plugins.plugins.Settings)}
>
settings of the Settings plugin
</Button>!
</Forms.FormText>
{Switches.map(s => s && (
<Switch
key={s.key}
value={settings[s.key]}
onChange={v => settings[s.key] = v}
note={s.note}
>
{s.title}
</Switch>
))}
</Forms.FormSection>
{needsVibrancySettings && <>
<Forms.FormTitle tag="h5">Window vibrancy style (requires restart)</Forms.FormTitle>
<Select
className={Margins.bottom20}
placeholder="Window vibrancy style"
options={[
// Sorted from most opaque to most transparent
{
label: "No vibrancy", value: undefined
},
{
label: "Under Page (window tinting)",
value: "under-page"
},
{
label: "Content",
value: "content"
},
{
label: "Window",
value: "window"
},
{
label: "Selection",
value: "selection"
},
{
label: "Titlebar",
value: "titlebar"
},
{
label: "Header",
value: "header"
},
{
label: "Sidebar",
value: "sidebar"
},
{
label: "Tooltip",
value: "tooltip"
},
{
label: "Menu",
value: "menu"
},
{
label: "Popover",
value: "popover"
},
{
label: "Fullscreen UI (transparent but slightly muted)",
value: "fullscreen-ui"
},
{
label: "HUD (Most transparent)",
value: "hud"
},
]}
select={v => settings.macosVibrancyStyle = v}
isSelected={v => settings.macosVibrancyStyle === v}
serialize={identity} />
</>}
<Forms.FormSection className={Margins.top16} title="Vencord Notifications" tag="h5">
<Flex>
<Button onClick={openNotificationSettingsModal}>
Notification Settings
</Button>
<Button onClick={openNotificationLogModal}>
View Notification Log
</Button>
</Flex>
</Forms.FormSection>
</SettingsTab>
);
}
function DonateButtonComponent() {
return (
<DonateButton
look={Button.Looks.FILLED}
color={Button.Colors.WHITE}
style={{ marginTop: "1em" }}
/>
);
}
function isDonor(userId: string): boolean {
const donorBadges = BadgeAPI.getDonorBadges(userId);
return GuildMemberStore.getMember(VENCORD_GUILD_ID, userId)?.roles.includes(DONOR_ROLE_ID) || !!donorBadges;
}
export default wrapTab(VencordSettings, "Vencord Settings");

View file

@ -4,14 +4,14 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export * from "./Badge";
export * from "./CheckedTextInput";
export * from "./CodeBlock";
export * from "./DonateButton";
export { default as ErrorBoundary } from "./ErrorBoundary";
export * from "./ErrorCard";
export * from "./Flex";
export * from "./Grid";
export * from "./Heart";
export * from "./Icons";
export * from "./Link";
export * from "./Switch";
export * from "./settings";

View file

@ -1,6 +1,7 @@
.vc-addon-card {
background-color: var(--background-base-lower-alt);
background-color: var(--card-primary-bg);
color: var(--interactive-active);
border: 1px solid var(--border-subtle);
border-radius: 8px;
display: block;
height: 100%;
@ -11,26 +12,15 @@
box-sizing: border-box;
}
.visual-refresh .vc-addon-card {
background-color: var(--card-primary-bg);
border: 1px solid var(--border-subtle);
}
.vc-addon-card-disabled {
opacity: 0.6;
}
.vc-addon-card:hover {
background-color: var(--background-tertiary);
transform: translateY(-1px);
box-shadow: var(--elevation-high);
}
.visual-refresh .vc-addon-card:hover {
/* same as non-hover, here to overwrite the non-refresh hover background */
background-color: var(--card-primary-bg);
}
.vc-addon-header {
margin-top: auto;
display: flex;

View file

@ -16,11 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./addonCard.css";
import "./AddonCard.css";
import { classNameFactory } from "@api/Styles";
import { Badge } from "@components/Badge";
import { Switch } from "@components/Switch";
import { AddonBadge } from "@components/settings/PluginBadge";
import { Switch } from "@components/settings/Switch";
import { Text, useRef } from "@webpack/common";
import type { MouseEventHandler, ReactNode } from "react";
@ -44,6 +44,7 @@ interface Props {
export function AddonCard({ disabled, isNew, name, infoButton, footer, author, enabled, setEnabled, description, onMouseEnter, onMouseLeave }: Props) {
const titleRef = useRef<HTMLDivElement>(null);
const titleContainerRef = useRef<HTMLDivElement>(null);
return (
<div
className={cl("card", { "card-disabled": disabled })}
@ -67,8 +68,10 @@ export function AddonCard({ disabled, isNew, name, infoButton, footer, author, e
>
{name}
</div>
</div>{isNew && <Badge text="NEW" color="#ED4245" />}
</div>
{isNew && <AddonBadge text="NEW" color="#ED4245" />}
</Text>
{!!author && (
<Text variant="text-md/normal" className={cl("author")}>
{author}

View file

@ -16,10 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Heart } from "@components/Heart";
import { ButtonProps } from "@vencord/discord-types";
import { Button } from "@webpack/common";
import { ButtonProps } from "@webpack/types";
import { Heart } from "./Heart";
export default function DonateButton({
look = Button.Looks.LINK,

View file

@ -16,9 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export function Badge({ text, color }) {
export function AddonBadge({ text, color }) {
return (
<div className="vc-plugins-badge" style={{
<div className="vc-addon-badge" style={{
backgroundColor: color,
justifySelf: "flex-end",
marginLeft: "auto"

View file

@ -14,7 +14,7 @@
.vc-settings-quickActions-pill {
all: unset;
background: var(--background-base-lower);
background: var(--button-secondary-background);
color: var(--header-secondary);
display: flex;
align-items: center;
@ -26,7 +26,7 @@
}
.vc-settings-quickActions-pill:hover {
background: var(--background-base-lower-alt);
background: var(--button-secondary-background-hover);
transform: translateY(-1px);
box-shadow: var(--elevation-high);
}
@ -36,14 +36,6 @@
outline-offset: 2px;
}
.visual-refresh .vc-settings-quickActions-pill {
background: var(--button-secondary-background);
}
.visual-refresh .vc-settings-quickActions-pill:hover {
background: var(--button-secondary-background-hover);
}
.vc-settings-quickActions-img {
width: 24px;
height: 24px;

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./quickActions.css";
import "./QuickAction.css";
import { classNameFactory } from "@api/Styles";
import { Card } from "@webpack/common";

View file

@ -12,11 +12,6 @@
position: relative;
}
.vc-settings-card {
padding: 1em;
margin-bottom: 1em;
}
.vc-special-card-special {
padding: 1em 1.5em;
margin-bottom: 1em;

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import "./specialCard.css";
import "./SpecialCard.css";
import { classNameFactory } from "@api/Styles";
import { Card, Clickable, Forms, React } from "@webpack/common";

Some files were not shown because too many files have changed in this diff Show more