├── .gitattributes ├── .vscode └── settings.json ├── FUNDING.yml ├── Discord ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── image-16x16.jpg │ │ ├── image-32x32.jpg │ │ ├── image-64x64.jpg │ │ ├── image-128x128.jpg │ │ ├── image-256x256 1.jpg │ │ ├── image-256x256.jpg │ │ ├── image-32x32 1.jpg │ │ ├── image-512x512 1.jpg │ │ ├── image-512x512.jpg │ │ ├── AppIcon512x512@2x.png │ │ └── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ └── Info.plist ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Settings │ ├── CustomCSSView.swift │ ├── SettingsView.swift │ ├── PluginsView.swift │ └── GeneralView.swift ├── Resources │ └── Plugins │ │ ├── AppleEmojis.js │ │ └── FakeNitro.js ├── Vars.swift ├── Info.plist ├── Extensions │ └── UserDefaults.swift ├── Discord.entitlements ├── DiscordWindowContent.swift ├── ContentView.swift ├── DiscordApp.swift ├── Plugin.swift ├── SecondaryWindowController.swift └── WebView.swift ├── LICENSE ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── github-releases-to-discord.yml ├── Package.swift ├── appcast.xml ├── Discord.entitlements ├── .gitignore ├── Info.plist ├── Voxa.xcodeproj ├── xcshareddata │ └── xcschemes │ │ └── Discord.xcscheme └── project.pbxproj ├── README.md └── CODE_OF_CONDUCT.md /.gitattributes: -------------------------------------------------------------------------------- 1 | **/*.node filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.indentSize": "tabSize", 3 | "editor.tabSize": 4 4 | } -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | 2 | ko_fi: plyght 3 | buy_me_a_coffee: plyght 4 | thanks_dev: # Replace with a single thanks.dev username 5 | -------------------------------------------------------------------------------- /Discord/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Discord/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Discord/Assets.xcassets/AppIcon.appiconset/image-16x16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxa-org/Voxa/HEAD/Discord/Assets.xcassets/AppIcon.appiconset/image-16x16.jpg -------------------------------------------------------------------------------- /Discord/Assets.xcassets/AppIcon.appiconset/image-32x32.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxa-org/Voxa/HEAD/Discord/Assets.xcassets/AppIcon.appiconset/image-32x32.jpg -------------------------------------------------------------------------------- /Discord/Assets.xcassets/AppIcon.appiconset/image-64x64.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxa-org/Voxa/HEAD/Discord/Assets.xcassets/AppIcon.appiconset/image-64x64.jpg -------------------------------------------------------------------------------- /Discord/Assets.xcassets/AppIcon.appiconset/image-128x128.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxa-org/Voxa/HEAD/Discord/Assets.xcassets/AppIcon.appiconset/image-128x128.jpg -------------------------------------------------------------------------------- /Discord/Assets.xcassets/AppIcon.appiconset/image-256x256 1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxa-org/Voxa/HEAD/Discord/Assets.xcassets/AppIcon.appiconset/image-256x256 1.jpg -------------------------------------------------------------------------------- /Discord/Assets.xcassets/AppIcon.appiconset/image-256x256.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxa-org/Voxa/HEAD/Discord/Assets.xcassets/AppIcon.appiconset/image-256x256.jpg -------------------------------------------------------------------------------- /Discord/Assets.xcassets/AppIcon.appiconset/image-32x32 1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxa-org/Voxa/HEAD/Discord/Assets.xcassets/AppIcon.appiconset/image-32x32 1.jpg -------------------------------------------------------------------------------- /Discord/Assets.xcassets/AppIcon.appiconset/image-512x512 1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxa-org/Voxa/HEAD/Discord/Assets.xcassets/AppIcon.appiconset/image-512x512 1.jpg -------------------------------------------------------------------------------- /Discord/Assets.xcassets/AppIcon.appiconset/image-512x512.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxa-org/Voxa/HEAD/Discord/Assets.xcassets/AppIcon.appiconset/image-512x512.jpg -------------------------------------------------------------------------------- /Discord/Assets.xcassets/AppIcon.appiconset/AppIcon512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxa-org/Voxa/HEAD/Discord/Assets.xcassets/AppIcon.appiconset/AppIcon512x512@2x.png -------------------------------------------------------------------------------- /Discord/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Discord/Settings/CustomCSSView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import WebKit 3 | 4 | struct CustomCSSView: View { 5 | @AppStorage("customCSS") private var customCSS: String = "" 6 | 7 | var body: some View { 8 | TextEditor(text: $customCSS) 9 | .padding() 10 | .onChange(of: customCSS) { 11 | Vars.webViewReference!.evaluateJavaScript( 12 | "document.getElementById('voxaCustomStyle').textContent = `\(customCSS)`;") 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Discord/Resources/Plugins/AppleEmojis.js: -------------------------------------------------------------------------------- 1 | // ==VoxaPlugin== 2 | // @name: Apple Emojis 3 | // @author: DevilBro 4 | // @description: Replaces Discord's Emojis with Apple's Emojis. 5 | // @url: https://github.com/mwittrien/BetterDiscordAddons/tree/master/Themes/EmojiReplace 6 | // ==/VoxaPlugin== 7 | 8 | const emojiStyle = document.createElement('style'); 9 | emojiStyle.textContent = `@import url(https://mwittrien.github.io/BetterDiscordAddons/Themes/EmojiReplace/base/Apple.css)`; 10 | document.head.appendChild(emojiStyle); 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /Discord/Assets.xcassets/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | NSMicrophoneUsageDescription 7 | Discord needs access to your microphone for voice chat and calls. 8 | NSCameraUsageDescription 9 | Discord needs access to your camera for video chat and calls. 10 | NSLocationWhenInUseUsageDescription 11 | Discord needs access to your location for certain features. 12 | 13 | 14 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Voxa", 8 | products: [ 9 | // Products define the executables and libraries a package produces, making them visible to other packages. 10 | .library( 11 | name: "Voxa", 12 | targets: ["Voxa"]) 13 | ], 14 | targets: [ 15 | // Targets are the basic building blocks of a package, defining a module or a test suite. 16 | // Targets can depend on other targets in this package and products from dependencies. 17 | .target( 18 | name: "Voxa") 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Voxa Releases 4 | https://github.com/plyght/voxa 5 | 6 | Version 0.2 7 | 8 | https://github.com/plyght/voxa/releases/tag/v0.2 9 | 10 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/github-releases-to-discord.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [published] 4 | 5 | jobs: 6 | github-releases-to-discord: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v3 11 | - name: GitHub Releases To Discord 12 | uses: SethCohen/github-releases-to-discord@v1 13 | with: 14 | webhook_url: ${{ secrets.WEBHOOK_URL }} 15 | color: "2105893" 16 | username: "Release Changelog" 17 | avatar_url: "https://example.com/avatar.png" 18 | content: "||@everyone||" 19 | footer_title: "Release Notification" 20 | footer_icon_url: "https://example.com/footer-icon.png" 21 | footer_timestamp: true 22 | max_description: '4096' 23 | reduce_headings: true -------------------------------------------------------------------------------- /Discord/Vars.swift: -------------------------------------------------------------------------------- 1 | import WebKit 2 | 3 | class Vars { 4 | static var webViewReference: WKWebView? 5 | } 6 | 7 | enum DiscordReleaseChannel: String, CaseIterable { 8 | case stable = "stable" 9 | case PTB = "ptb" 10 | case canary = "canary" 11 | 12 | var description: String { 13 | switch self { 14 | case .stable: 15 | return "Stable" 16 | case .PTB: 17 | return "Public Test Branch (PTB)" 18 | case .canary: 19 | return "Canary" 20 | } 21 | } 22 | 23 | var url: URL { 24 | switch self { 25 | case .stable: 26 | return URL(string: "https://discord.com/app")! 27 | case .PTB: 28 | return URL(string: "https://ptb.discord.com/app")! 29 | case .canary: 30 | return URL(string: "https://canary.discord.com/app")! 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Discord/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleExecutable 6 | Voxa 7 | CFBundleName 8 | Voxa 9 | CFBundleDocumentTypes 10 | 11 | 12 | CFBundleTypeIconSystemGenerated 13 | 1 14 | CFBundleTypeName 15 | Voxa 16 | CFBundleTypeRole 17 | Viewer 18 | LSHandlerRank 19 | Default 20 | 21 | 22 | CFBundleIconFile 23 | 24 | LSMinimumSystemVersion 25 | $(MACOSX_DEPLOYMENT_TARGET) 26 | NSAppTransportSecurity 27 | 28 | NSAllowsArbitraryLoads 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /Discord/Settings/SettingsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsView: View { 4 | @State private var selectedItem: String? = "general" 5 | 6 | var body: some View { 7 | NavigationSplitView(columnVisibility: .constant(.doubleColumn)) { 8 | List(selection: $selectedItem) { 9 | NavigationLink(value: "general") { 10 | Label("General", systemImage: "gear") 11 | } 12 | NavigationLink(value: "plugins") { 13 | Label("Plugins", systemImage: "puzzlepiece.extension") 14 | } 15 | NavigationLink(value: "customcss") { 16 | Label("Custom CSS", systemImage: "paintbrush") 17 | } 18 | } 19 | .padding(.top) 20 | .frame(width: 215) 21 | .toolbar(removing: .sidebarToggle) 22 | } detail: { 23 | switch selectedItem { 24 | case "general": GeneralView() 25 | case "plugins": PluginsView() 26 | case "customcss": CustomCSSView() 27 | default: Text("") 28 | } 29 | } 30 | .frame(minWidth: 715, maxWidth: 715, minHeight: 470, maxHeight: .infinity) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Discord/Extensions/UserDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults.swift 3 | // Mythic 4 | // 5 | // Created by vapidinfinity (esi) on 26/5/2024. 6 | // 7 | 8 | // Copyright © 2024 vapidinfinity 9 | 10 | // From Mythic. (https://getmythic.app/) 11 | 12 | import Foundation 13 | 14 | extension UserDefaults { 15 | @discardableResult 16 | func encodeAndSet(_ data: T, forKey key: String) throws -> Data { 17 | let encodedData = try PropertyListEncoder().encode(data) 18 | set(encodedData, forKey: key) 19 | return encodedData 20 | } 21 | 22 | @discardableResult 23 | func encodeAndRegister(defaults registrationDictionary: [String: Encodable]) throws -> [String: Any] { 24 | for (key, value) in registrationDictionary { 25 | let encodedData = try PropertyListEncoder().encode(value) 26 | register(defaults: [key: encodedData]) 27 | } 28 | 29 | return dictionaryRepresentation() 30 | } 31 | 32 | func decodeAndGet(_ type: T.Type, forKey key: String) throws -> T? { 33 | guard let data = data(forKey: key) else { return nil } 34 | let decodedData = try PropertyListDecoder().decode(T.self, from: data) 35 | return decodedData 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Discord/Discord.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.automation.apple-events 6 | 7 | com.apple.security.cs.allow-dyld-environment-variables 8 | 9 | com.apple.security.cs.allow-jit 10 | 11 | com.apple.security.cs.allow-unsigned-executable-memory 12 | 13 | com.apple.security.device.audio-input 14 | 15 | com.apple.security.device.camera 16 | 17 | com.apple.security.device.microphone 18 | 19 | com.apple.security.device.usb 20 | 21 | com.apple.security.personal-information.photos-library 22 | 23 | com.apple.security.temporary-exception.mach-lookup.global-name 24 | 25 | com.apple.WebKit.WebContent 26 | com.apple.WebKit.WebContent.Development 27 | com.apple.WebKit.GPU 28 | com.apple.WebKit.Networking 29 | 30 | com.apple.security.temporary-exception.sbpl 31 | 32 | (allow mach-lookup (global-name-regex #"^com.apple.WebKit.*)) 33 | (allow mach-lookup (global-name "com.apple.audio.AudioComponentRegistrar")) 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Discord.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.device.camera 6 | 7 | com.apple.security.device.microphone 8 | 9 | com.apple.security.device.audio-input 10 | 11 | com.apple.security.device.usb 12 | 13 | com.apple.security.cs.allow-jit 14 | 15 | com.apple.security.cs.allow-unsigned-executable-memory 16 | 17 | com.apple.security.cs.allow-dyld-environment-variables 18 | 19 | com.apple.security.temporary-exception.mach-lookup.global-name 20 | 21 | com.apple.WebKit.WebContent 22 | com.apple.WebKit.WebContent.Development 23 | com.apple.WebKit.GPU 24 | com.apple.WebKit.Networking 25 | 26 | com.apple.security.temporary-exception.sbpl 27 | 28 | (allow mach-lookup (global-name-regex #"^com.apple.WebKit.*)) 29 | (allow mach-lookup (global-name "com.apple.audio.AudioComponentRegistrar")) 30 | 31 | com.apple.security.automation.apple-events 32 | 33 | 34 | -------------------------------------------------------------------------------- /Discord/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "image-16x16.jpg", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "image-32x32 1.jpg", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "image-32x32.jpg", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "image-64x64.jpg", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "image-128x128.jpg", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "image-256x256 1.jpg", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "image-256x256.jpg", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "image-512x512 1.jpg", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "image-512x512.jpg", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "AppIcon512x512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | DerivedData/ 3 | *.xcworkspace/ 4 | xcuserdata/ 5 | *.xcodeproj/* 6 | !*.xcodeproj/project.pbxproj 7 | !*.xcodeproj/xcshareddata/**/* 8 | *.xcscmblueprint 9 | 10 | # Swift Package Manager 11 | .build/ 12 | .swiftpm/xcode/package.xcworkspace/ 13 | Package.resolved 14 | 15 | # Cocoapods 16 | Pods/ 17 | Podfile.lock 18 | 19 | # Carthage 20 | Carthage/Build/ 21 | 22 | # Accio 23 | Dependencies/ 24 | .accio/ 25 | 26 | # Fastlane 27 | fastlane/report.xml 28 | fastlane/Preview.html 29 | fastlane/screenshots/ 30 | fastlane/test_output/ 31 | fastlane/tools/ 32 | 33 | # CocoaPods Keys 34 | *.xcconfig 35 | 36 | # Archives 37 | *.xcarchive 38 | 39 | # App data 40 | *.appex.dSYM 41 | *.dSYM 42 | 43 | # Playground 44 | timeline.xctimeline 45 | playground.xcworkspace 46 | 47 | # Swift pm 48 | .swiftpm/ 49 | 50 | # Bundle artifacts 51 | *.ipa 52 | *.app.dSYM.zip 53 | 54 | # Logs 55 | *.log 56 | 57 | # Dependency manager 58 | Cartfile.resolved 59 | 60 | # Other 61 | *.DS_Store 62 | *.swp 63 | *.lock 64 | *.xcuserstate 65 | *.moved-aside 66 | *.hmap 67 | *.ipa 68 | *.app.dSYM 69 | *.xcuserdatad/ 70 | 71 | # macOS specific files 72 | .DS_Store 73 | .AppleDouble 74 | .LSOverride 75 | 76 | # Thumbnails 77 | ._* 78 | 79 | # Files that might appear on external disk 80 | .Spotlight-V100 81 | .Trashes 82 | 83 | # Directories potentially created on remote AFP share 84 | .AppleDB 85 | .AppleDesktop 86 | 87 | # Temporary files 88 | .TemporaryItems 89 | .apdisk 90 | 91 | # SwiftLint 92 | .swiftlint.yml 93 | 94 | # Xcode logs 95 | *.xccovreport 96 | *.xccovreport/* 97 | *.xccovarchive 98 | *.xccovarchive/* 99 | 100 | # Index 101 | *.index -------------------------------------------------------------------------------- /Discord/DiscordWindowContent.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import WebKit 3 | import UnixDomainSocket 4 | 5 | struct DiscordWindowContent: View { 6 | var channelClickWidth: CGFloat 7 | @AppStorage("discordReleaseChannel") private var discordReleaseChannel: String = "stable" 8 | 9 | // Reference to the underlying WKWebView 10 | @State var webViewReference: WKWebView? 11 | 12 | var body: some View { 13 | ZStack(alignment: .topLeading) { 14 | // Main background & web content 15 | ZStack { 16 | // Add a subtle system effect 17 | VisualEffectView(material: .sidebar, blendingMode: .behindWindow) 18 | 19 | // Embed the Discord WebView 20 | WebView( 21 | channelClickWidth: channelClickWidth, 22 | initialURL: DiscordReleaseChannel.allCases.first(where: { $0.rawValue == discordReleaseChannel })!.url, 23 | webViewReference: $webViewReference 24 | ) 25 | .frame(maxWidth: .infinity, maxHeight: .infinity) 26 | .onChange(of: webViewReference) { 27 | Vars.webViewReference = webViewReference 28 | } 29 | } 30 | 31 | // Draggable area for traffic lights 32 | DraggableView() 33 | .frame(height: 48) 34 | } 35 | .ignoresSafeArea() 36 | .frame(maxWidth: .infinity, maxHeight: .infinity) 37 | .onDisappear { 38 | // *Analysis*: If you wanted to do cleanup or set webViewReference = nil, you could do so here. 39 | print("DiscordWindowContent disappeared.") 40 | } 41 | } 42 | } 43 | 44 | #Preview { 45 | DiscordWindowContent(channelClickWidth: 1000) 46 | } 47 | -------------------------------------------------------------------------------- /Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSMicrophoneUsageDescription 6 | Discord needs access to your microphone for voice chat and calls. 7 | NSCameraUsageDescription 8 | Discord needs access to your camera for video chat and calls. 9 | NSLocationWhenInUseUsageDescription 10 | Discord needs access to your location for certain features. 11 | CFBundleDocumentTypes 12 | 13 | 14 | CFBundleTypeName 15 | Discord 16 | LSHandlerRank 17 | Default 18 | 19 | 20 | CFBundleExecutable 21 | $(EXECUTABLE_NAME) 22 | CFBundleIconFile 23 | 24 | CFBundleIdentifier 25 | $(PRODUCT_BUNDLE_IDENTIFIER) 26 | CFBundleInfoDictionaryVersion 27 | 6.0 28 | CFBundleName 29 | $(PRODUCT_NAME) 30 | CFBundlePackageType 31 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 32 | CFBundleShortVersionString 33 | 1.0 34 | CFBundleVersion 35 | 1 36 | LSMinimumSystemVersion 37 | $(MACOSX_DEPLOYMENT_TARGET) 38 | NSAppTransportSecurity 39 | 40 | NSAllowsArbitraryLoads 41 | 42 | 43 | LSApplicationCategoryType 44 | public.app-category.social-networking 45 | 46 | -------------------------------------------------------------------------------- /Discord/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Discord 4 | // 5 | // Created by Austin Thomas on 24/11/2024. 6 | // 7 | 8 | import SwiftUI 9 | import AppKit 10 | 11 | struct ContentView: View { 12 | // *Analysis*: Add minimal logging or user guidance 13 | var body: some View { 14 | // If you wanted a small text area or overlay, you could do it here 15 | DiscordWindowContent(channelClickWidth: 1000) 16 | .onAppear { 17 | print("ContentView has appeared.") 18 | } 19 | } 20 | } 21 | 22 | struct DraggableView: NSViewRepresentable { 23 | class Coordinator: NSObject { 24 | @objc func handlePanGesture(_ gesture: NSPanGestureRecognizer) { 25 | guard let window = gesture.view?.window, let event = NSApp.currentEvent else { return } 26 | 27 | switch gesture.state { 28 | case .began, .changed: 29 | window.performDrag(with: event) 30 | default: 31 | break 32 | } 33 | } 34 | } 35 | 36 | func makeNSView(context: Context) -> NSView { 37 | let view = NSView() 38 | view.wantsLayer = true 39 | view.layer?.backgroundColor = .clear 40 | 41 | // Ensure the view is above others and can receive mouse events 42 | view.translatesAutoresizingMaskIntoConstraints = false 43 | view.layer?.zPosition = 999 44 | 45 | let panGesture = NSPanGestureRecognizer( 46 | target: context.coordinator, 47 | action: #selector(Coordinator.handlePanGesture(_:)) 48 | ) 49 | panGesture.allowedTouchTypes = [.direct] 50 | view.addGestureRecognizer(panGesture) 51 | 52 | return view 53 | } 54 | 55 | func updateNSView(_ nsView: NSView, context: Context) {} 56 | 57 | func makeCoordinator() -> Coordinator { 58 | Coordinator() 59 | } 60 | } 61 | 62 | struct VisualEffectView: NSViewRepresentable { 63 | let material: NSVisualEffectView.Material 64 | let blendingMode: NSVisualEffectView.BlendingMode 65 | 66 | func makeNSView(context: Context) -> NSVisualEffectView { 67 | let visualEffectView = NSVisualEffectView() 68 | visualEffectView.material = material 69 | visualEffectView.blendingMode = blendingMode 70 | return visualEffectView 71 | } 72 | 73 | func updateNSView(_ visualEffectView: NSVisualEffectView, context: Context) { 74 | visualEffectView.material = material 75 | visualEffectView.blendingMode = blendingMode 76 | } 77 | } 78 | 79 | #Preview { 80 | ContentView() 81 | } 82 | -------------------------------------------------------------------------------- /Discord/Settings/PluginsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import WebKit 3 | 4 | struct PluginsView: View { 5 | @State public var pluginsChanged: Bool = false 6 | 7 | var body: some View { 8 | Form { 9 | ForEach(availablePlugins) { plugin in 10 | PluginListItem( 11 | plugin: plugin, 12 | pluginsChanged: $pluginsChanged 13 | ) 14 | } 15 | } 16 | .formStyle(.grouped) 17 | 18 | if pluginsChanged { 19 | Form { 20 | HStack { 21 | Text("Refresh Voxa to Apply Changes") 22 | Spacer() 23 | Button("Refresh") { 24 | hardReloadWebView(webView: Vars.webViewReference!) 25 | 26 | withAnimation { 27 | pluginsChanged = false 28 | } 29 | } 30 | } 31 | } 32 | .formStyle(.grouped) 33 | } 34 | } 35 | } 36 | 37 | struct PluginListItem: View { 38 | let plugin: Plugin 39 | @Binding var pluginsChanged: Bool 40 | 41 | var body: some View { 42 | Toggle( 43 | isOn: Binding( 44 | get: { activePlugins.contains(plugin) }, 45 | set: { isActive in 46 | if isActive { 47 | activePlugins.append(plugin) 48 | } else { 49 | activePlugins.removeAll(where: { $0 == plugin }) 50 | } 51 | 52 | withAnimation { 53 | pluginsChanged = true 54 | } 55 | } 56 | ) 57 | ) { 58 | Section { 59 | HStack { 60 | Text(plugin.name) 61 | .foregroundStyle(.primary) 62 | 63 | if let url = plugin.url { 64 | Button { 65 | NSWorkspace.shared.open(url) 66 | } label: { 67 | Image(systemName: "globe") 68 | .foregroundColor(.blue) 69 | } 70 | .buttonStyle(.plain) 71 | } 72 | } 73 | } footer: { 74 | Text(plugin.author) 75 | .foregroundStyle(.secondary) 76 | Text(plugin.description) 77 | .foregroundStyle(.tertiary) 78 | } 79 | } 80 | } 81 | } 82 | 83 | #Preview { 84 | PluginsView() 85 | } 86 | -------------------------------------------------------------------------------- /Discord/DiscordApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiscordApp.swift 3 | // Discord 4 | // 5 | // Created by Austin Thomas on 24/11/2024. 6 | // 7 | 8 | import AppKit 9 | import Foundation 10 | import SwiftUI 11 | import UnixDomainSocket 12 | 13 | class WindowDelegate: NSObject, NSWindowDelegate { 14 | func windowDidResize(_ notification: Notification) { 15 | repositionTrafficLights(for: notification) 16 | } 17 | 18 | func windowDidEndLiveResize(_ notification: Notification) { 19 | repositionTrafficLights(for: notification) 20 | } 21 | 22 | func windowDidMove(_ notification: Notification) { 23 | repositionTrafficLights(for: notification) 24 | } 25 | 26 | func windowDidLayout(_ notification: Notification) { 27 | repositionTrafficLights(for: notification) 28 | } 29 | 30 | func windowDidBecomeKey(_ notification: Notification) { 31 | repositionTrafficLights(for: notification) 32 | } 33 | 34 | private func repositionTrafficLights(for notification: Notification) { 35 | guard let window = notification.object as? NSWindow else { return } 36 | 37 | let repositionBlock = { 38 | window.standardWindowButton(.closeButton)?.isHidden = false 39 | window.standardWindowButton(.miniaturizeButton)?.isHidden = false 40 | window.standardWindowButton(.zoomButton)?.isHidden = false 41 | 42 | // Position traffic lights 43 | window.standardWindowButton(.closeButton)?.setFrameOrigin(NSPoint(x: 10, y: -5)) 44 | window.standardWindowButton(.miniaturizeButton)?.setFrameOrigin(NSPoint(x: 30, y: -5)) 45 | window.standardWindowButton(.zoomButton)?.setFrameOrigin(NSPoint(x: 50, y: -5)) 46 | } 47 | 48 | // Execute immediately 49 | repositionBlock() 50 | 51 | // And after a slight delay (0.1 s) to catch any animation completions 52 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 53 | repositionBlock() 54 | } 55 | } 56 | } 57 | 58 | @main 59 | struct DiscordApp: App { 60 | @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 61 | 62 | var body: some Scene { 63 | WindowGroup { 64 | ContentView() 65 | .frame(minWidth: 800, minHeight: 400) 66 | .onAppear { 67 | // Use a guard to ensure there's a main screen 68 | if NSScreen.main == nil { 69 | print("No available main screen to set initial window frame.") 70 | return 71 | } 72 | 73 | // If there's a main application window, configure it 74 | if let window = NSApplication.shared.windows.first { 75 | // Configure window for resizing 76 | window.styleMask.insert(.resizable) 77 | 78 | // Assign delegate for traffic light positioning 79 | window.delegate = appDelegate.windowDelegate 80 | } 81 | } 82 | } 83 | .windowStyle(.hiddenTitleBar) 84 | .commands { 85 | CommandGroup(replacing: .newItem) { 86 | Button("Reload") { 87 | hardReloadWebView(webView: Vars.webViewReference!) 88 | } 89 | .keyboardShortcut("r", modifiers: .command) 90 | } 91 | } 92 | 93 | Settings { 94 | SettingsView() 95 | } 96 | } 97 | } 98 | 99 | class AppDelegate: NSObject, NSApplicationDelegate { 100 | let windowDelegate = WindowDelegate() 101 | } 102 | -------------------------------------------------------------------------------- /Discord/Plugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Plugin.swift 3 | // Discord 4 | // 5 | // Created by vapidinfinity (esi) on 27/1/2025. 6 | // 7 | 8 | import Foundation 9 | 10 | var activePlugins: [Plugin] { 11 | get { 12 | do { 13 | let pluginURLs = try UserDefaults.standard.decodeAndGet([URL].self, forKey: "activePluginURLs") ?? [] 14 | return try pluginURLs.compactMap({ try Plugin(fileURL: $0) }) 15 | } catch { 16 | print("Error fetching active plugins from UserDefaults: \(error)") 17 | } 18 | 19 | return [] 20 | } 21 | set { 22 | do { 23 | try UserDefaults.standard.encodeAndSet(newValue.map(\.fileURL), forKey: "activePluginURLs") 24 | } catch { 25 | print("Error storing active plugins in UserDefaults: \(error)") 26 | } 27 | } 28 | } 29 | 30 | var availablePlugins: [Plugin] { 31 | guard let resources = Bundle.main.resourceURL else { 32 | return [] 33 | } 34 | var plugins: [Plugin] = [] 35 | 36 | let resourceContents = (try? FileManager.default.contentsOfDirectory(atPath: resources.path)) ?? [] 37 | 38 | for file in resourceContents where file.hasSuffix(".js") { 39 | let fileURL = resources.appending(path: file) 40 | 41 | var isDir: ObjCBool = false 42 | if FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDir), !isDir.boolValue { 43 | do { 44 | let plugin = try Plugin(fileURL: fileURL) 45 | plugins.append(plugin) 46 | } catch { 47 | print("Couldn't fetch plugin at \(fileURL.path): \(error.localizedDescription)") 48 | } 49 | } 50 | } 51 | 52 | return plugins 53 | } 54 | 55 | struct Plugin: Identifiable, Equatable, Codable { 56 | static func == (lhs: Plugin, rhs: Plugin) -> Bool { 57 | return lhs.fileURL == rhs.fileURL 58 | } 59 | 60 | var id: UUID = UUID() 61 | var name: String = "Unknown" 62 | var author: String = "Unknown" 63 | var description: String = "Unknown" 64 | var url: URL? 65 | 66 | var contents: String = "" 67 | var fileURL: URL 68 | 69 | 70 | init(fileURL: URL) throws { 71 | self.fileURL = fileURL 72 | 73 | let rawPlugin = try String(contentsOfFile: fileURL.path, encoding: .utf8) 74 | let lines = rawPlugin.split(whereSeparator: \.isNewline) 75 | 76 | guard 77 | let initialLine = lines.firstIndex(where: {(try? Regex("==[^=]+==").firstMatch(in: String($0)) != nil) ?? false }), 78 | let terminalLine = lines.firstIndex(where: { (try? Regex("==/[^=]+==").firstMatch(in: String($0)) != nil) ?? false }) 79 | else { 80 | return 81 | } 82 | 83 | for line in lines[initialLine...terminalLine] { 84 | guard 85 | let match = try? Regex(#"@(\w+):? ([^\n]+)"#).firstMatch(in: String(line)), 86 | let label = match[1].substring, 87 | let content = match[2].substring 88 | else { 89 | continue 90 | } 91 | 92 | switch label { 93 | case "name": 94 | self.name = String(content) 95 | case "author": 96 | self.author = String(content) 97 | case "description": 98 | self.description = String(content) 99 | case "url": 100 | self.url = URL(string: String(content)) 101 | default: 102 | print("Unhandled Plugin header label \"\(label)\"; ignoring.") 103 | } 104 | } 105 | 106 | self.contents = lines[terminalLine ..< lines.endIndex].joined(separator: "\n") 107 | } 108 | 109 | struct ExtractionError: LocalizedError { } 110 | } 111 | -------------------------------------------------------------------------------- /Voxa.xcodeproj/xcshareddata/xcschemes/Discord.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 61 | 62 | 63 | 67 | 68 | 69 | 70 | 76 | 79 | 80 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /Discord/SecondaryWindowController.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import SwiftUI 3 | import WebKit 4 | 5 | class SecondaryWindow: NSWindow { 6 | override func awakeFromNib() { 7 | super.awakeFromNib() 8 | positionTrafficLights() 9 | } 10 | 11 | override func setFrame(_ frameRect: NSRect, display flag: Bool, animate animateFlag: Bool) { 12 | super.setFrame(frameRect, display: flag, animate: animateFlag) 13 | positionTrafficLights() 14 | } 15 | 16 | override func makeKeyAndOrderFront(_ sender: Any?) { 17 | super.makeKeyAndOrderFront(sender) 18 | positionTrafficLights() 19 | } 20 | 21 | override func makeKey() { 22 | super.makeKey() 23 | positionTrafficLights() 24 | } 25 | 26 | override func makeMain() { 27 | super.makeMain() 28 | positionTrafficLights() 29 | } 30 | 31 | private func positionTrafficLights() { 32 | DispatchQueue.main.async { [weak self] in 33 | guard let self = self else { return } 34 | 35 | // Force layout if needed 36 | self.layoutIfNeeded() 37 | 38 | // Position each button with a slight delay to ensure they're ready 39 | let buttons: [(NSWindow.ButtonType, CGPoint)] = [ 40 | (.closeButton, NSPoint(x: 10, y: -5)), 41 | (.miniaturizeButton, NSPoint(x: 30, y: -5)), 42 | (.zoomButton, NSPoint(x: 50, y: -5)), 43 | ] 44 | 45 | for (buttonType, point) in buttons { 46 | if let button = self.standardWindowButton(buttonType) { 47 | button.isHidden = false 48 | button.setFrameOrigin(point) 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | class SecondaryWindowController: NSWindowController { 56 | convenience init(url: String, channelClickWidth: CGFloat) { 57 | let window = SecondaryWindow( 58 | contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), 59 | styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], 60 | backing: .buffered, 61 | defer: false 62 | ) 63 | 64 | // Configure window appearance 65 | window.titlebarAppearsTransparent = true 66 | window.titleVisibility = .hidden 67 | window.toolbarStyle = .unifiedCompact 68 | window.backgroundColor = .clear 69 | 70 | // Create the SwiftUI view for the window with custom CSS 71 | let contentView = SecondaryWindowView(url: url, channelClickWidth: channelClickWidth) 72 | window.contentView = NSHostingView(rootView: contentView) 73 | 74 | self.init(window: window) 75 | 76 | // Use the shared window delegate from AppDelegate 77 | if let appDelegate = NSApplication.shared.delegate as? AppDelegate { 78 | window.delegate = appDelegate.windowDelegate 79 | } 80 | 81 | // Ensure traffic lights are visible 82 | window.standardWindowButton(.closeButton)?.isHidden = false 83 | window.standardWindowButton(.miniaturizeButton)?.isHidden = false 84 | window.standardWindowButton(.zoomButton)?.isHidden = false 85 | } 86 | } 87 | 88 | struct SecondaryWindowView: View { 89 | let url: String 90 | let channelClickWidth: CGFloat 91 | 92 | var body: some View { 93 | DiscordWindowContent( 94 | channelClickWidth: channelClickWidth 95 | ) 96 | .frame(minWidth: 200, minHeight: 200) 97 | } 98 | } 99 | 100 | struct SecondaryWindowScene: Scene { 101 | let url: String 102 | let channelClickWidth: CGFloat 103 | 104 | var body: some Scene { 105 | WindowGroup { 106 | SecondaryWindowView(url: url, channelClickWidth: channelClickWidth) 107 | } 108 | .windowStyle(.hiddenTitleBar) 109 | .windowResizability(.contentSize) 110 | .defaultPosition(.center) 111 | .defaultSize(width: 800, height: 600) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Discord/Settings/GeneralView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct GeneralView: View { 4 | @AppStorage("discordUsesSystemAccent") private var fullSystemAccent: Bool = true 5 | @AppStorage("discordSidebarDividerUsesSystemAccent") private var sidebarDividerSystemAccent: Bool = true 6 | @AppStorage("discordReleaseChannel") private var discordReleaseChannel: String = "stable" 7 | @State private var discordReleaseChannelSelection: DiscordReleaseChannel = .stable 8 | 9 | // ===== PROXY SUPPORT ADDED ===== 10 | @AppStorage("useDiscordProxy") private var useDiscordProxy: Bool = false 11 | @AppStorage("discordProxyAddress") private var discordProxyAddress: String = "" 12 | // ================================ 13 | 14 | var body: some View { 15 | ScrollView { 16 | Form { 17 | HStack { 18 | Text("Join The Discord") 19 | Spacer() 20 | Button("Join Discord") { 21 | let link = URL(string:"https://discord.gg/Dps8HnDBpw")! 22 | let request = URLRequest(url: link) 23 | Vars.webViewReference!.load(request) 24 | } 25 | } 26 | 27 | HStack { 28 | Text("Support Us On GitHub") 29 | Spacer() 30 | Button("Go To Voxa's GitHub") { 31 | let url = URL(string: "https://github.com/plyght/Voxa")! 32 | NSWorkspace.shared.open(url) 33 | } 34 | } 35 | 36 | Toggle(isOn: $fullSystemAccent) { 37 | Text("Voxa matches system accent color") 38 | Text("Modifying this setting will reload Voxa.") 39 | .foregroundStyle(.placeholder) 40 | } 41 | .onChange(of: fullSystemAccent, { hardReloadWebView(webView: Vars.webViewReference!) }) 42 | 43 | Toggle(isOn: $sidebarDividerSystemAccent) { 44 | Text("Sidebar divider matches system accent color") 45 | Text("Modifying this setting will reload Voxa.") 46 | .foregroundStyle(.placeholder) 47 | } 48 | .onChange(of: sidebarDividerSystemAccent, { hardReloadWebView(webView: Vars.webViewReference!) }) 49 | 50 | Picker(selection: $discordReleaseChannelSelection, content: { 51 | ForEach(DiscordReleaseChannel.allCases, id: \.self) { 52 | Text($0.description) 53 | } 54 | }, label: { 55 | Text("Discord Release Channel") 56 | Text("Modifying this setting will reload Voxa.") 57 | .foregroundStyle(.placeholder) 58 | }) 59 | .onChange(of: discordReleaseChannelSelection) { 60 | switch discordReleaseChannelSelection { 61 | case .stable: 62 | discordReleaseChannel = "stable" 63 | case .PTB: 64 | discordReleaseChannel = "ptb" 65 | case .canary: 66 | discordReleaseChannel = "canary" 67 | } 68 | } 69 | .onChange(of: discordReleaseChannelSelection, { hardReloadWebView(webView: Vars.webViewReference!) }) 70 | 71 | // ===== PROXY SUPPORT ADDED ===== 72 | Section(header: Text("Proxy Settings")) { 73 | Toggle("Use Proxy for Discord", isOn: $useDiscordProxy) 74 | if useDiscordProxy { 75 | TextField("Enter proxy URL (e.g. http://proxy.example.com:8080)", 76 | text: $discordProxyAddress) 77 | .textFieldStyle(RoundedBorderTextFieldStyle()) 78 | } 79 | } 80 | // ================================ 81 | } 82 | .formStyle(.grouped) 83 | } 84 | } 85 | } 86 | 87 | #Preview { 88 | GeneralView() 89 | } 90 | -------------------------------------------------------------------------------- /Discord/Resources/Plugins/FakeNitro.js: -------------------------------------------------------------------------------- 1 | // ==VoxaPlugin== 2 | // @name: FakeNitro 3 | // @author: Stossy11 4 | // @description: Simulates Nitro. 5 | // ==/VoxaPlugin== 6 | 7 | let z; 8 | let isEnabled = true; // Flag to control the script's execution 9 | 10 | function loader() { 11 | if (!isEnabled) { 12 | return; // If the script is disabled, do nothing 13 | } 14 | 15 | window.webpackChunkdiscord_app.push([ 16 | [Math.random()], {}, 17 | e => { 18 | window.wpRequire = e; 19 | } 20 | ]); 21 | 22 | let e = () => Object.keys(wpRequire.c).map((e => wpRequire.c[e].exports)).filter((e => e)), 23 | t = t => { 24 | for (const n of e()) { 25 | if (n.default && t(n.default)) return n.default; 26 | if (n.Z && t(n.Z)) return n.Z; 27 | if (t(n)) return n; 28 | } 29 | }, 30 | n = t => { 31 | let n = []; 32 | for (const s of e()) s.default && t(s.default) ? n.push(s.default) : t(s) && n.push(s); 33 | return n; 34 | }, 35 | s = (...e) => t((t => e.every((e => void 0 !== t[e])))), 36 | a = (...e) => n((t => e.every((e => void 0 !== t[e])))), 37 | r = e => new Promise((t => setTimeout(t, e))); 38 | 39 | if (!s("getCurrentUser").getCurrentUser()) { 40 | return; 41 | } else { 42 | clearInterval(z); 43 | } 44 | 45 | s("getCurrentUser").getCurrentUser().premiumType = 2; 46 | let i = s("sendMessage"); 47 | i.__sendMessage = i.__sendMessage || i._sendMessage; 48 | 49 | i._sendMessage = async function(e, t, n) { 50 | // Handle emoji replacements 51 | if (t?.validNonShortcutEmojis?.length > 0) { 52 | t.validNonShortcutEmojis.forEach((emoji) => { 53 | const emojiRegex = new RegExp(`<(a|):${emoji.originalName || emoji.name}:${emoji.id}>`, 'g'); 54 | // Construct the URL with size=48 55 | const emojiUrl = emoji.animated ? 56 | `https://cdn.discordapp.com/emojis/${emoji.id}.gif?size=48` : 57 | `https://cdn.discordapp.com/emojis/${emoji.id}.png?size=48`; 58 | // Replace the emoji with its name wrapped in a Markdown-style link 59 | t.content = t.content.replace(emojiRegex, `[${emoji.name}](${emojiUrl})`); 60 | }); 61 | } 62 | 63 | // Handle stickers 64 | if (n?.stickerIds?.length > 0) { 65 | n.stickerIds.forEach((stickerId) => { 66 | t.content = t.content + " https://media.discordapp.net/stickers/" + stickerId + ".webp?size=160"; 67 | }); 68 | n = { 69 | ...n, 70 | stickerIds: undefined 71 | }; 72 | } 73 | 74 | // Handle message length splitting 75 | if (t.content.length > 2000) { 76 | let a = t.content.split(/([\S\s]{1,2000})/g); 77 | 78 | // Handle code block splitting 79 | if (a[1].match(/```/g)?.length % 2 !== 0 && a[3].length <= 1980) { 80 | let e = a[1]; 81 | a[1] = e.substring(0, 1997) + "```"; 82 | let t = a[1].match(/```[^\n ]+/g); 83 | t = t[t.length % 2 === 0 ? t.length - 2 : t.length - 1].replace("```", ""); 84 | let n = "```"; 85 | a[3].match(/```/g)?.length >= 1 && a[3].match(/```/g)?.length % 2 !== 0 && (n = ""); 86 | a[3] = "```" + t + "\n" + e.substring(1997, 2000) + a[3] + n; 87 | } 88 | 89 | // Send split messages 90 | let l = s("getCachedChannelJsonForGuild").getChannel(e).rateLimitPerUser; 91 | await i.__sendMessage.bind(i)(e, { 92 | ...t, 93 | content: a[1] 94 | }, n); 95 | 96 | let o = false; 97 | while (!o) { 98 | await r(l); 99 | let s = i.__sendMessage.bind(i)(e, { 100 | ...t, 101 | content: a[3] 102 | }, n).catch((e) => { 103 | l = 1000 * e.body.retry_after; 104 | o = false; 105 | }); 106 | if (s = await s, s?.ok) return await s; 107 | } 108 | } 109 | 110 | return await i.__sendMessage.bind(i)(e, t, n); 111 | }; 112 | } 113 | 114 | z = setInterval(loader, 1); 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Voxa 2 | 3 | Voxa is a sleek, lightweight, native macOS Discord client built using SwiftUI. Designed for speed, efficiency, and a modern user experience, it brings the best of Discord to your Mac in a fully optimized package. 4 | 5 | ## This project is currently not functioning for users with the new Discord UI until the next release. 6 | 7 | # [⚠️ Disclaimer!](#disclaimer) 8 | 9 | ![SCR-20250116-mutv](https://github.com/user-attachments/assets/68c662e9-368b-4e9d-9d8a-ffde4e1b6c06) 10 | 11 | ## This project wouldn't have started without [@AustinGraphics](https://github.com/AustinGraphics), go support him! 12 | 13 | ![](https://img.shields.io/github/downloads/plyght/voxa/total?style=social&logoColor=000000) 14 | 15 | ### Discord Server 16 | 17 | https://discord.gg/Dps8HnDBpw 18 | 19 | ### Matrix Room 20 | 21 | https://matrix.to/#/#voxa:matrix.org 22 | 23 | ### Website (in development)1. 24 | 25 | https://voxa.peril.lol 26 | 27 | ## Key Features 28 | 29 | - **Native macOS Experience:** Built with SwiftUI, Voxa integrates seamlessly with macOS features for a polished and fast user interface. 30 | - **Performance-Focused:** Lightweight design ensures low resource usage and high responsiveness. 31 | - **Customizable Interface:** Modify appearance with custom CSS and theming. 32 | - **Advanced Window Management:** Resizable and draggable windows with transparency and aesthetic layouts. 33 | - **Privacy-First Permissions:** Fine-grained microphone, camera, and location access are available only when needed. 34 | - **FakeNitro and other Vencord plugins:** FakeNitro and Apple Emojis have recently been implemented into Voxa and other BetterDiscord/Vencord plugins are expected soon! See [Upcoming Features](#upcoming-features). 35 | 36 | ## Known Issues 37 | 38 | - Some items are fully transparent, lacking any translucency or blur effects. 39 | - Certain items have not been fully integrated with transparent/translucent features. 40 | - Light mode has a few minor, easily resolvable issues. 41 | - To fix this, your macOS settings need to be the same as your Voxa settings. For example, if I want to use light mode on Voxa, my system settings also need to be on light mode. 42 | 43 | --- 44 | 45 | ## Table of Contents 46 | 47 | - [Installation](#installation) 48 | - [Upcoming Features](#upcoming-features) 49 | - [Disclaimer](#disclaimer) 50 | 51 | --- 52 | 53 | ## Installation 54 | 55 | ### Installer 56 | ```shell 57 | curl -sL https://voxa.peril.lol/install | bash 58 | ``` 59 | you can view the install script here: https://github.com/voxa-org/voxa.peril.lol/blob/main/public/scripts/install 60 | 61 | ### Using a Prebuilt Release 62 | 63 | 1. Download the latest `.dmg` file from the [Releases page](https://github.com/plyght/voxa/releases). 64 | 2. Open the `.dmg` file and drag the Voxa app to your `Applications` folder. 65 | 3. Launch Voxa from your Applications folder. 66 | 67 | ### Building From Source 68 | 69 | 1. Clone the repository: 70 | ```bash 71 | git clone https://github.com/plyght/Voxa.git 72 | cd Voxa 73 | ``` 74 | 75 | ## For Upcoming Features, See The [Voxa Roadmap](https://github.com/users/plyght/projects/3) 76 | 77 | # Disclaimer! 78 | Client modifications are against Discord’s Terms of Service. 79 | 80 | However, Discord is pretty indifferent about them, and there are no known cases of users getting banned for using client mods! So you should generally be fine as long as you don’t use any plugins that implement abusive behaviour. 81 | 82 | Regardless, if your account is very important to you and getting disabled would be a disaster for you, you should probably not use any client mods (not exclusive to Voxa), just to be safe. 83 | 84 | Additionally, make sure not to post screenshots with Voxa in a server where you might get banned for it 85 | 86 | 87 | 88 | ## Star History 89 | 90 | 91 | 92 | 93 | 94 | Star History Chart 95 | 96 | 97 | 98 | ##### 1[voxa.peril.lol Source Code](https://github.com/plyght/voxa.peril.lol) - This contains the source code of the Voxa website. 99 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | plyght@peril.lol. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Voxa.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6A821BF82D4EA44600A3A9E4 /* DiscordRPCBridge in Frameworks */ = {isa = PBXBuildFile; productRef = 6A821BF72D4EA44600A3A9E4 /* DiscordRPCBridge */; }; 11 | 6A821C252D4EA98B00A3A9E4 /* DiscordRPCBridge in Frameworks */ = {isa = PBXBuildFile; productRef = 6A821C242D4EA98B00A3A9E4 /* DiscordRPCBridge */; }; 12 | DC9455282D28B7FC0023E2BB /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = DC9455272D28B7FC0023E2BB /* Sparkle */; }; 13 | /* End PBXBuildFile section */ 14 | 15 | /* Begin PBXFileReference section */ 16 | D910A0852CF2B6F8005F6119 /* Voxa.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Voxa.app; sourceTree = BUILT_PRODUCTS_DIR; }; 17 | /* End PBXFileReference section */ 18 | 19 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 20 | DCE2C6F82D37F3C300CFF277 /* Exceptions for "Discord" folder in "Discord" target */ = { 21 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 22 | membershipExceptions = ( 23 | Info.plist, 24 | ); 25 | target = D910A0842CF2B6F8005F6119 /* Discord */; 26 | }; 27 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ 28 | 29 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 30 | D910A0872CF2B6F8005F6119 /* Discord */ = { 31 | isa = PBXFileSystemSynchronizedRootGroup; 32 | exceptions = ( 33 | DCE2C6F82D37F3C300CFF277 /* Exceptions for "Discord" folder in "Discord" target */, 34 | ); 35 | path = Discord; 36 | sourceTree = ""; 37 | }; 38 | /* End PBXFileSystemSynchronizedRootGroup section */ 39 | 40 | /* Begin PBXFrameworksBuildPhase section */ 41 | D910A0822CF2B6F8005F6119 /* Frameworks */ = { 42 | isa = PBXFrameworksBuildPhase; 43 | buildActionMask = 2147483647; 44 | files = ( 45 | 6A821BF82D4EA44600A3A9E4 /* DiscordRPCBridge in Frameworks */, 46 | DC9455282D28B7FC0023E2BB /* Sparkle in Frameworks */, 47 | 6A821C252D4EA98B00A3A9E4 /* DiscordRPCBridge in Frameworks */, 48 | ); 49 | runOnlyForDeploymentPostprocessing = 0; 50 | }; 51 | /* End PBXFrameworksBuildPhase section */ 52 | 53 | /* Begin PBXGroup section */ 54 | D910A07C2CF2B6F8005F6119 = { 55 | isa = PBXGroup; 56 | children = ( 57 | D910A0872CF2B6F8005F6119 /* Discord */, 58 | D910A0862CF2B6F8005F6119 /* Products */, 59 | ); 60 | sourceTree = ""; 61 | }; 62 | D910A0862CF2B6F8005F6119 /* Products */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | D910A0852CF2B6F8005F6119 /* Voxa.app */, 66 | ); 67 | name = Products; 68 | sourceTree = ""; 69 | }; 70 | /* End PBXGroup section */ 71 | 72 | /* Begin PBXNativeTarget section */ 73 | D910A0842CF2B6F8005F6119 /* Discord */ = { 74 | isa = PBXNativeTarget; 75 | buildConfigurationList = D910A0942CF2B6FA005F6119 /* Build configuration list for PBXNativeTarget "Discord" */; 76 | buildPhases = ( 77 | D910A0812CF2B6F8005F6119 /* Sources */, 78 | D910A0822CF2B6F8005F6119 /* Frameworks */, 79 | D910A0832CF2B6F8005F6119 /* Resources */, 80 | ); 81 | buildRules = ( 82 | ); 83 | dependencies = ( 84 | ); 85 | fileSystemSynchronizedGroups = ( 86 | D910A0872CF2B6F8005F6119 /* Discord */, 87 | ); 88 | name = Discord; 89 | packageProductDependencies = ( 90 | DC9455272D28B7FC0023E2BB /* Sparkle */, 91 | 6A821BF72D4EA44600A3A9E4 /* DiscordRPCBridge */, 92 | 6A821C242D4EA98B00A3A9E4 /* DiscordRPCBridge */, 93 | ); 94 | productName = Discord; 95 | productReference = D910A0852CF2B6F8005F6119 /* Voxa.app */; 96 | productType = "com.apple.product-type.application"; 97 | }; 98 | /* End PBXNativeTarget section */ 99 | 100 | /* Begin PBXProject section */ 101 | D910A07D2CF2B6F8005F6119 /* Project object */ = { 102 | isa = PBXProject; 103 | attributes = { 104 | BuildIndependentTargetsInParallel = 1; 105 | LastSwiftUpdateCheck = 1610; 106 | LastUpgradeCheck = 1620; 107 | TargetAttributes = { 108 | D910A0842CF2B6F8005F6119 = { 109 | CreatedOnToolsVersion = 16.1; 110 | }; 111 | }; 112 | }; 113 | buildConfigurationList = D910A0802CF2B6F8005F6119 /* Build configuration list for PBXProject "Voxa" */; 114 | developmentRegion = en; 115 | hasScannedForEncodings = 0; 116 | knownRegions = ( 117 | en, 118 | Base, 119 | ); 120 | mainGroup = D910A07C2CF2B6F8005F6119; 121 | minimizedProjectReferenceProxies = 1; 122 | packageReferences = ( 123 | DC9455262D28B7FC0023E2BB /* XCRemoteSwiftPackageReference "Sparkle" */, 124 | 6A821C232D4EA98B00A3A9E4 /* XCRemoteSwiftPackageReference "DiscordRPCBridge" */, 125 | ); 126 | preferredProjectObjectVersion = 77; 127 | productRefGroup = D910A0862CF2B6F8005F6119 /* Products */; 128 | projectDirPath = ""; 129 | projectRoot = ""; 130 | targets = ( 131 | D910A0842CF2B6F8005F6119 /* Discord */, 132 | ); 133 | }; 134 | /* End PBXProject section */ 135 | 136 | /* Begin PBXResourcesBuildPhase section */ 137 | D910A0832CF2B6F8005F6119 /* Resources */ = { 138 | isa = PBXResourcesBuildPhase; 139 | buildActionMask = 2147483647; 140 | files = ( 141 | ); 142 | runOnlyForDeploymentPostprocessing = 0; 143 | }; 144 | /* End PBXResourcesBuildPhase section */ 145 | 146 | /* Begin PBXSourcesBuildPhase section */ 147 | D910A0812CF2B6F8005F6119 /* Sources */ = { 148 | isa = PBXSourcesBuildPhase; 149 | buildActionMask = 2147483647; 150 | files = ( 151 | ); 152 | runOnlyForDeploymentPostprocessing = 0; 153 | }; 154 | /* End PBXSourcesBuildPhase section */ 155 | 156 | /* Begin XCBuildConfiguration section */ 157 | D910A0922CF2B6FA005F6119 /* Debug */ = { 158 | isa = XCBuildConfiguration; 159 | buildSettings = { 160 | ALWAYS_SEARCH_USER_PATHS = NO; 161 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 162 | CLANG_ANALYZER_NONNULL = YES; 163 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 164 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 165 | CLANG_ENABLE_MODULES = YES; 166 | CLANG_ENABLE_OBJC_ARC = YES; 167 | CLANG_ENABLE_OBJC_WEAK = YES; 168 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 169 | CLANG_WARN_BOOL_CONVERSION = YES; 170 | CLANG_WARN_COMMA = YES; 171 | CLANG_WARN_CONSTANT_CONVERSION = YES; 172 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 173 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 174 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 175 | CLANG_WARN_EMPTY_BODY = YES; 176 | CLANG_WARN_ENUM_CONVERSION = YES; 177 | CLANG_WARN_INFINITE_RECURSION = YES; 178 | CLANG_WARN_INT_CONVERSION = YES; 179 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 180 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 181 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 182 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 183 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 184 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 185 | CLANG_WARN_STRICT_PROTOTYPES = YES; 186 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 187 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 188 | CLANG_WARN_UNREACHABLE_CODE = YES; 189 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 190 | COPY_PHASE_STRIP = NO; 191 | DEAD_CODE_STRIPPING = YES; 192 | DEBUG_INFORMATION_FORMAT = dwarf; 193 | ENABLE_STRICT_OBJC_MSGSEND = YES; 194 | ENABLE_TESTABILITY = YES; 195 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 196 | GCC_C_LANGUAGE_STANDARD = gnu17; 197 | GCC_DYNAMIC_NO_PIC = NO; 198 | GCC_NO_COMMON_BLOCKS = YES; 199 | GCC_OPTIMIZATION_LEVEL = 0; 200 | GCC_PREPROCESSOR_DEFINITIONS = ( 201 | "DEBUG=1", 202 | "$(inherited)", 203 | ); 204 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 205 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 206 | GCC_WARN_UNDECLARED_SELECTOR = YES; 207 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 208 | GCC_WARN_UNUSED_FUNCTION = YES; 209 | GCC_WARN_UNUSED_VARIABLE = YES; 210 | GENERATE_INFOPLIST_FILE = NO; 211 | INFOPLIST_FILE = Discord/Info.plist; 212 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 213 | MACOSX_DEPLOYMENT_TARGET = 15.1; 214 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 215 | MTL_FAST_MATH = YES; 216 | ONLY_ACTIVE_ARCH = YES; 217 | PRODUCT_NAME = Voxa; 218 | SDKROOT = macosx; 219 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 220 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 221 | }; 222 | name = Debug; 223 | }; 224 | D910A0932CF2B6FA005F6119 /* Release */ = { 225 | isa = XCBuildConfiguration; 226 | buildSettings = { 227 | ALWAYS_SEARCH_USER_PATHS = NO; 228 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 229 | CLANG_ANALYZER_NONNULL = YES; 230 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 231 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 232 | CLANG_ENABLE_MODULES = YES; 233 | CLANG_ENABLE_OBJC_ARC = YES; 234 | CLANG_ENABLE_OBJC_WEAK = YES; 235 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 236 | CLANG_WARN_BOOL_CONVERSION = YES; 237 | CLANG_WARN_COMMA = YES; 238 | CLANG_WARN_CONSTANT_CONVERSION = YES; 239 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 240 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 241 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 242 | CLANG_WARN_EMPTY_BODY = YES; 243 | CLANG_WARN_ENUM_CONVERSION = YES; 244 | CLANG_WARN_INFINITE_RECURSION = YES; 245 | CLANG_WARN_INT_CONVERSION = YES; 246 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 247 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 248 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 249 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 250 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 251 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 252 | CLANG_WARN_STRICT_PROTOTYPES = YES; 253 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 254 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 255 | CLANG_WARN_UNREACHABLE_CODE = YES; 256 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 257 | COPY_PHASE_STRIP = NO; 258 | DEAD_CODE_STRIPPING = YES; 259 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 260 | ENABLE_NS_ASSERTIONS = NO; 261 | ENABLE_STRICT_OBJC_MSGSEND = YES; 262 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 263 | GCC_C_LANGUAGE_STANDARD = gnu17; 264 | GCC_NO_COMMON_BLOCKS = YES; 265 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 266 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 267 | GCC_WARN_UNDECLARED_SELECTOR = YES; 268 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 269 | GCC_WARN_UNUSED_FUNCTION = YES; 270 | GCC_WARN_UNUSED_VARIABLE = YES; 271 | GENERATE_INFOPLIST_FILE = NO; 272 | INFOPLIST_FILE = Discord/Info.plist; 273 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 274 | MACOSX_DEPLOYMENT_TARGET = 15.1; 275 | MTL_ENABLE_DEBUG_INFO = NO; 276 | MTL_FAST_MATH = YES; 277 | PRODUCT_NAME = Voxa; 278 | SDKROOT = macosx; 279 | SWIFT_COMPILATION_MODE = wholemodule; 280 | }; 281 | name = Release; 282 | }; 283 | D910A0952CF2B6FA005F6119 /* Debug */ = { 284 | isa = XCBuildConfiguration; 285 | buildSettings = { 286 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 287 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 288 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 289 | CODE_SIGN_ENTITLEMENTS = Discord/Discord.entitlements; 290 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 291 | CODE_SIGN_STYLE = Automatic; 292 | COMBINE_HIDPI_IMAGES = YES; 293 | CURRENT_PROJECT_VERSION = 1; 294 | DEAD_CODE_STRIPPING = YES; 295 | DEVELOPMENT_ASSET_PATHS = "\"Discord/Preview Content\""; 296 | DEVELOPMENT_TEAM = ""; 297 | ENABLE_HARDENED_RUNTIME = YES; 298 | ENABLE_PREVIEWS = YES; 299 | GENERATE_INFOPLIST_FILE = YES; 300 | INFOPLIST_KEY_CFBundleDisplayName = Voxa; 301 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; 302 | INFOPLIST_KEY_NSCameraUsageDescription = "Need camera access for uploading images"; 303 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 304 | INFOPLIST_KEY_NSLocationUsageDescription = "Need location access for updating nearby friends"; 305 | INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "This app will use your location to show features near you."; 306 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "Need microphone access for uploading audio"; 307 | INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Need photo library access for saving and uploading images"; 308 | LD_RUNPATH_SEARCH_PATHS = ( 309 | "$(inherited)", 310 | "@executable_path/../Frameworks", 311 | ); 312 | MACOSX_DEPLOYMENT_TARGET = 15.1; 313 | MARKETING_VERSION = 1.0; 314 | PRODUCT_BUNDLE_IDENTIFIER = lol.peril.voxa; 315 | PRODUCT_NAME = Voxa; 316 | SWIFT_EMIT_LOC_STRINGS = YES; 317 | SWIFT_VERSION = 5.0; 318 | }; 319 | name = Debug; 320 | }; 321 | D910A0962CF2B6FA005F6119 /* Release */ = { 322 | isa = XCBuildConfiguration; 323 | buildSettings = { 324 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 325 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 326 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 327 | CODE_SIGN_ENTITLEMENTS = Discord/Discord.entitlements; 328 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 329 | CODE_SIGN_STYLE = Automatic; 330 | COMBINE_HIDPI_IMAGES = YES; 331 | CURRENT_PROJECT_VERSION = 1; 332 | DEAD_CODE_STRIPPING = YES; 333 | DEVELOPMENT_ASSET_PATHS = "\"Discord/Preview Content\""; 334 | DEVELOPMENT_TEAM = ""; 335 | ENABLE_HARDENED_RUNTIME = YES; 336 | ENABLE_PREVIEWS = YES; 337 | GENERATE_INFOPLIST_FILE = YES; 338 | INFOPLIST_KEY_CFBundleDisplayName = Voxa; 339 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; 340 | INFOPLIST_KEY_NSCameraUsageDescription = "Need camera access for uploading images"; 341 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 342 | INFOPLIST_KEY_NSLocationUsageDescription = "Need location access for updating nearby friends"; 343 | INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "This app will use your location to show features near you."; 344 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "Need microphone access for uploading audio"; 345 | INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Need photo library access for saving and uploading images"; 346 | LD_RUNPATH_SEARCH_PATHS = ( 347 | "$(inherited)", 348 | "@executable_path/../Frameworks", 349 | ); 350 | MACOSX_DEPLOYMENT_TARGET = 15.1; 351 | MARKETING_VERSION = 1.0; 352 | PRODUCT_BUNDLE_IDENTIFIER = lol.peril.voxa; 353 | PRODUCT_NAME = Voxa; 354 | SWIFT_EMIT_LOC_STRINGS = YES; 355 | SWIFT_VERSION = 5.0; 356 | }; 357 | name = Release; 358 | }; 359 | /* End XCBuildConfiguration section */ 360 | 361 | /* Begin XCConfigurationList section */ 362 | D910A0802CF2B6F8005F6119 /* Build configuration list for PBXProject "Voxa" */ = { 363 | isa = XCConfigurationList; 364 | buildConfigurations = ( 365 | D910A0922CF2B6FA005F6119 /* Debug */, 366 | D910A0932CF2B6FA005F6119 /* Release */, 367 | ); 368 | defaultConfigurationIsVisible = 0; 369 | defaultConfigurationName = Release; 370 | }; 371 | D910A0942CF2B6FA005F6119 /* Build configuration list for PBXNativeTarget "Discord" */ = { 372 | isa = XCConfigurationList; 373 | buildConfigurations = ( 374 | D910A0952CF2B6FA005F6119 /* Debug */, 375 | D910A0962CF2B6FA005F6119 /* Release */, 376 | ); 377 | defaultConfigurationIsVisible = 0; 378 | defaultConfigurationName = Release; 379 | }; 380 | /* End XCConfigurationList section */ 381 | 382 | /* Begin XCRemoteSwiftPackageReference section */ 383 | 6A821C232D4EA98B00A3A9E4 /* XCRemoteSwiftPackageReference "DiscordRPCBridge" */ = { 384 | isa = XCRemoteSwiftPackageReference; 385 | repositoryURL = "https://github.com/vapidinfinity/DiscordRPCBridge"; 386 | requirement = { 387 | kind = upToNextMajorVersion; 388 | minimumVersion = 1.0.1; 389 | }; 390 | }; 391 | DC9455262D28B7FC0023E2BB /* XCRemoteSwiftPackageReference "Sparkle" */ = { 392 | isa = XCRemoteSwiftPackageReference; 393 | repositoryURL = "https://github.com/sparkle-project/Sparkle"; 394 | requirement = { 395 | kind = upToNextMajorVersion; 396 | minimumVersion = 2.6.4; 397 | }; 398 | }; 399 | /* End XCRemoteSwiftPackageReference section */ 400 | 401 | /* Begin XCSwiftPackageProductDependency section */ 402 | 6A821BF72D4EA44600A3A9E4 /* DiscordRPCBridge */ = { 403 | isa = XCSwiftPackageProductDependency; 404 | productName = DiscordRPCBridge; 405 | }; 406 | 6A821C242D4EA98B00A3A9E4 /* DiscordRPCBridge */ = { 407 | isa = XCSwiftPackageProductDependency; 408 | package = 6A821C232D4EA98B00A3A9E4 /* XCRemoteSwiftPackageReference "DiscordRPCBridge" */; 409 | productName = DiscordRPCBridge; 410 | }; 411 | DC9455272D28B7FC0023E2BB /* Sparkle */ = { 412 | isa = XCSwiftPackageProductDependency; 413 | package = DC9455262D28B7FC0023E2BB /* XCRemoteSwiftPackageReference "Sparkle" */; 414 | productName = Sparkle; 415 | }; 416 | /* End XCSwiftPackageProductDependency section */ 417 | }; 418 | rootObject = D910A07D2CF2B6F8005F6119 /* Project object */; 419 | } 420 | -------------------------------------------------------------------------------- /Discord/WebView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import DiscordRPCBridge 3 | import Foundation 4 | import UserNotifications 5 | import OSLog 6 | @preconcurrency import WebKit 7 | 8 | // MARK: - Constants 9 | 10 | /// CSS for accent color customization 11 | var hexAccentColor: String? { 12 | if let accentColor = NSColor.controlAccentColor.usingColorSpace(.sRGB) { 13 | let red = Int(accentColor.redComponent * 255) 14 | let green = Int(accentColor.greenComponent * 255) 15 | let blue = Int(accentColor.blueComponent * 255) 16 | return String(format: "#%02X%02X%02X", red, green, blue) 17 | } 18 | return nil 19 | } 20 | 21 | /// Non-dynamic default CSS applied to the webview. 22 | let rootCSS = """ 23 | :root { 24 | --background-accent: rgba(0, 0, 0, 0.5) !important; 25 | --background-floating: transparent !important; 26 | --background-message-highlight: transparent !important; 27 | --background-message-highlight-hover: transparent !important; 28 | --background-message-hover: transparent !important; 29 | --background-mobile-primary: transparent !important; 30 | --background-mobile-secondary: transparent !important; 31 | --background-modifier-accent: transparent !important; 32 | --background-modifier-active: transparent !important; 33 | --background-modifier-hover: transparent !important; 34 | --background-modifier-selected: transparent !important; 35 | --background-nested-floating: transparent !important; 36 | --background-primary: transparent !important; 37 | --background-secondary: transparent !important; 38 | --background-secondary-alt: transparent !important; 39 | --background-tertiary: transparent !important; 40 | --bg-overlay-3: transparent !important; 41 | --channeltextarea-background: transparent !important; 42 | --background-secondary-alt: transparent !important; 43 | } 44 | """ 45 | 46 | struct SuffixedCSSStyle: Codable { 47 | let prefix: String 48 | let styles: [String: String] 49 | } 50 | 51 | /// CSS Styles that are sent to a script to automatically be suffixed and updated dynamically. 52 | /// You may explicitly add suffixes if necessary (e.g. if there are multiple objects that share the same prefix) 53 | var suffixedCSSStyles: [String: [String: String]] = [ 54 | "guilds": [ 55 | "margin-top": "48px" 56 | ], 57 | "scroller": [ 58 | "padding-top": "none", 59 | "mask-image": "linear-gradient(to bottom, black calc(100% - 36px), transparent 100%)", 60 | ], 61 | "themed_fc4f04": [ 62 | "background-color": "transparent" 63 | ], 64 | "themed__9293f": [ 65 | "background-color": "transparent" 66 | ], 67 | "button_df39bd": [ 68 | "background-color": "rgba(0, 0, 0, 0.15)" 69 | ], 70 | "chatContent": [ 71 | "background-color": "transparent", 72 | "background": "transparent" 73 | ], 74 | "chat": [ 75 | "background": "transparent" 76 | ], 77 | "quickswitcher": [ 78 | "background-color": "transparent", 79 | "-webkit-backdrop-filter": "blur(5px)" 80 | ], 81 | "content": [ 82 | "background": "none" 83 | ], 84 | "container": [ 85 | "background-color": "transparent" 86 | ], 87 | "mainCard": [ 88 | "background-color": "rgba(0, 0, 0, 0.15)" 89 | ], 90 | "listItem_c96c45:has(div[aria-label='Download Apps'])": [ 91 | "display": "none" 92 | ], 93 | "children_fc4f04:after": [ 94 | "background": "0", 95 | "width": "0" 96 | ], 97 | "expandedFolderBackground": [ 98 | "background": "var(--activity-card-background)" 99 | ], 100 | "folder": [ 101 | "background": "var(--activity-card-background)" 102 | ], 103 | "floating": [ 104 | "background": "var(--activity-card-background)" 105 | ], 106 | "content_f75fb0:before": [ 107 | "display": "none" 108 | ], 109 | "outer": [ 110 | "background-color": "transparent" 111 | ], 112 | "container__55c99": [ 113 | "background-color": "rgba(0, 0, 0, 0.65)", 114 | "-webkit-backdrop-filter": "blur(10px)" 115 | ] 116 | ] 117 | 118 | // MARK: - Utility Functions 119 | 120 | /// Retrieves the contents of a plugin file 121 | func getPluginContents(name fileName: String) -> String { 122 | if let filePath = Bundle.main.path(forResource: fileName, ofType: "js") { 123 | do { 124 | return try String(contentsOfFile: filePath, encoding: .utf8) 125 | } catch { 126 | print("Error reading plugin file contents: \(error.localizedDescription)") 127 | } 128 | } 129 | return "" 130 | } 131 | 132 | // MARK: - Plugin and CSS Loader 133 | 134 | /// Loads plugins and CSS into the provided WebView 135 | func loadPluginsAndCSS(webView: WKWebView) { 136 | @AppStorage("discordUsesSystemAccent") var fullSystemAccent: Bool = true 137 | @AppStorage("discordSidebarDividerUsesSystemAccent") var sidebarDividerSystemAccent: Bool = true 138 | 139 | let dynamicRootCSS = """ 140 | /* CSS variables that require reinitialisation on view reload */ 141 | \({ 142 | guard let accent = hexAccentColor, 143 | fullSystemAccent == true else { 144 | return "" 145 | } 146 | 147 | return """ 148 | :root { 149 | /* brand */ 150 | --bg-brand: \(accent) !important; 151 | \({ () -> String in 152 | var values = [String]() 153 | for i in stride(from: 5, through: 95, by: 5) { 154 | let hexAlpha = String(format: "%02X", Int(round((Double(i) / 100.0) * 255))) 155 | values.append("--brand-\(String(format: "%02d", i))a: \(accent)\(hexAlpha);") 156 | } 157 | return values.joined(separator: "\n") 158 | }()) 159 | --brand-260: \(accent)1A !important; 160 | --brand-500: \(accent) !important; 161 | --brand-560: \(accent)26 !important; /* filled button hover */ 162 | --brand-600: \(accent)30 !important; /* filled button clicked */ 163 | 164 | /* foregrounds */ 165 | --mention-foreground: \(accent) !important; 166 | --mention-background: \(accent)26 !important; 167 | --control-brand-foreground: \(accent)32 !important; 168 | --control-brand-foreground-new: \(accent)30 !important; 169 | } 170 | """ 171 | }()) 172 | """ 173 | 174 | // Also requires re-initialisation on view reload 175 | suffixedCSSStyles["guildSeparator"] = [ 176 | "background-color": { 177 | guard let accent = hexAccentColor, 178 | sidebarDividerSystemAccent == true else { 179 | return """ 180 | color-mix(/* --background-modifier-accent */ 181 | in oklab, 182 | hsl(var(--primary-500-hsl) / 0.48) 100%, 183 | hsl(var(--theme-base-color-hsl, 0 0% 0%) / 0.48) var(--theme-base-color-amount, 0%) 184 | ) 185 | """ 186 | } 187 | return accent 188 | }() 189 | ] 190 | 191 | // Inject default CSS 192 | webView.configuration.userContentController.addUserScript( 193 | WKUserScript( 194 | source: """ 195 | const defaultStyle = document.createElement('style'); 196 | defaultStyle.id = 'voxaStyle'; 197 | defaultStyle.textContent = `\(rootCSS + "\n\n" + dynamicRootCSS)`; 198 | document.head.appendChild(defaultStyle); 199 | 200 | const customStyle = document.createElement('style'); 201 | customStyle.id = 'voxaCustomStyle'; 202 | customStyle.textContent = ""; 203 | document.head.appendChild(customStyle); 204 | """, 205 | injectionTime: .atDocumentEnd, 206 | forMainFrameOnly: true 207 | ) 208 | ) 209 | 210 | let prefixStyles = suffixedCSSStyles.map { SuffixedCSSStyle(prefix: $0.key, styles: $0.value) } 211 | 212 | guard let styleData: Data = { 213 | do { 214 | return try JSONEncoder().encode(prefixStyles) 215 | } catch { 216 | print("Error encoding CSS styles to JSON: \(error)") 217 | return nil 218 | } 219 | }(), let styles = String(data: styleData, encoding: .utf8) else { 220 | print("Error converting style data to JSON string") 221 | return 222 | } 223 | 224 | let escapedStyles = styles 225 | .replacingOccurrences(of: #"\"#, with: #"\\"#) 226 | 227 | webView.configuration.userContentController.addUserScript( 228 | WKUserScript( 229 | source: """ 230 | (function() { 231 | const prefixes = JSON.parse(`\(escapedStyles)`); 232 | if (!prefixes.length) { 233 | console.log("No prefixes provided."); 234 | return; 235 | } 236 | 237 | // Each prefix maps to a Set of matching classes 238 | const classSets = prefixes.map(() => new Set()); 239 | 240 | function processElementClasses(element) { 241 | element.classList.forEach(cls => { 242 | prefixes.forEach((prefixConfig, index) => { 243 | const { prefix, styles } = prefixConfig; 244 | if (cls.startsWith(prefix + '_') || cls === prefix) { 245 | classSets[index].add(cls); 246 | applyImportantStyles(element, styles); 247 | } 248 | }); 249 | }); 250 | } 251 | 252 | function applyImportantStyles(element, styles) { 253 | for (const [prop, val] of Object.entries(styles)) { 254 | element.style.setProperty(prop, val, 'important'); 255 | } 256 | } 257 | 258 | function buildPrefixCSS(prefixConfigs) { 259 | let cssOutput = ''; 260 | for (const { prefix, styles } of prefixConfigs) { 261 | const hasSpace = prefix.includes(' '); 262 | const placeholder = hasSpace ? prefix : `${prefix}_placeholder`; 263 | cssOutput += `.${placeholder} {\n`; 264 | for (const [prop, val] of Object.entries(styles)) { 265 | cssOutput += ` ${prop}: ${val} !important;\n`; 266 | } 267 | cssOutput += `}\n\n`; 268 | } 269 | return cssOutput; 270 | } 271 | 272 | function showParsedCSS() { 273 | console.log(`Generated CSS from JSON:\n${buildPrefixCSS(prefixes)}`); 274 | } 275 | 276 | // Initial pass over all elements 277 | document.querySelectorAll('*').forEach(processElementClasses); 278 | 279 | // Monitor DOM changes 280 | const observer = new MutationObserver(mutations => { 281 | mutations.forEach(mutation => { 282 | if (mutation.type === 'childList') { 283 | mutation.addedNodes.forEach(node => { 284 | if (node.nodeType === Node.ELEMENT_NODE) { 285 | processElementClasses(node); 286 | node.querySelectorAll('*').forEach(processElementClasses); 287 | } 288 | }); 289 | } else if (mutation.type === 'attributes' && mutation.attributeName === 'class') { 290 | processElementClasses(mutation.target); 291 | } 292 | }); 293 | }); 294 | 295 | observer.observe(document.body, { childList: true, attributes: true, subtree: true }); 296 | 297 | function displayClassReports() { 298 | prefixes.forEach((prefixConfig, index) => { 299 | const { prefix } = prefixConfig; 300 | const matchedClasses = classSets[index]; 301 | if (matchedClasses.size > 0) { 302 | console.log(`Matching classes for prefix "${prefix}":`); 303 | matchedClasses.forEach(cls => console.log(cls)); 304 | } else { 305 | console.log(`No matching classes found for prefix "${prefix}".`); 306 | } 307 | }); 308 | } 309 | 310 | // Initial log 311 | displayClassReports(); 312 | // Re-log classes periodically 313 | setInterval(displayClassReports, 2000); 314 | 315 | // Expose CSS viewer 316 | window.showParsedCSS = showParsedCSS; 317 | })(); 318 | """, 319 | injectionTime: .atDocumentEnd, 320 | forMainFrameOnly: true 321 | ) 322 | ) 323 | 324 | // Load active plugins 325 | activePlugins.forEach { plugin in 326 | webView.configuration.userContentController.addUserScript( 327 | WKUserScript( 328 | source: plugin.contents, 329 | injectionTime: .atDocumentEnd, 330 | forMainFrameOnly: true 331 | ) 332 | ) 333 | } 334 | } 335 | 336 | // MARK: - WebView Representable 337 | 338 | struct WebView: NSViewRepresentable { 339 | var channelClickWidth: CGFloat 340 | var initialURL: URL 341 | @Binding var webViewReference: WKWebView? 342 | private let rpcBridge = DiscordRPCBridge() 343 | 344 | // ===== PROXY SUPPORT ADDED ===== 345 | @AppStorage("useDiscordProxy") private var useDiscordProxy: Bool = false 346 | @AppStorage("discordProxyAddress") private var discordProxyAddress: String = "" 347 | // ================================ 348 | 349 | // Initializers 350 | init(channelClickWidth: CGFloat, initialURL: URL) { 351 | self.channelClickWidth = channelClickWidth 352 | self.initialURL = initialURL 353 | self._webViewReference = .constant(nil) 354 | } 355 | 356 | init(channelClickWidth: CGFloat, initialURL: URL, webViewReference: Binding) { 357 | self.channelClickWidth = channelClickWidth 358 | self.initialURL = initialURL 359 | self._webViewReference = webViewReference 360 | } 361 | 362 | func makeCoordinator() -> Coordinator { 363 | Coordinator(self) 364 | } 365 | 366 | func makeNSView(context: Context) -> WKWebView { 367 | // MARK: WebView Configuration 368 | 369 | let config = WKWebViewConfiguration() 370 | 371 | config.applicationNameForUserAgent = "Version/17.2.1 Safari/605.1.15" 372 | 373 | // Enable media capture 374 | config.mediaTypesRequiringUserActionForPlayback = [] 375 | config.allowsAirPlayForMediaPlayback = true 376 | 377 | // If macOS 14 or higher, enable fullscreen 378 | if #available(macOS 14.0, *) { 379 | config.preferences.isElementFullscreenEnabled = true 380 | } 381 | 382 | // Additional media constraints 383 | config.preferences.setValue(true, forKey: "mediaDevicesEnabled") 384 | config.preferences.setValue(true, forKey: "mediaStreamEnabled") 385 | config.preferences.setValue(true, forKey: "peerConnectionEnabled") 386 | config.preferences.setValue(true, forKey: "screenCaptureEnabled") 387 | 388 | // Allow inspector while app is running in DEBUG 389 | #if DEBUG 390 | config.preferences.setValue(true, forKey: "developerExtrasEnabled") 391 | #endif 392 | 393 | // Edit CSP to allow for 3rd party scripts and stylesheets to be loaded 394 | config.setValue( 395 | "default-src * 'unsafe-inline' 'unsafe-eval'; script-src * 'unsafe-inline' 'unsafe-eval'; connect-src * 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; frame-src *; style-src * 'unsafe-inline';", 396 | forKey: "overrideContentSecurityPolicy" 397 | ) 398 | 399 | // ===== PROXY SUPPORT ADDED ===== 400 | if useDiscordProxy, 401 | let proxyURL = URL(string: discordProxyAddress), 402 | let host = proxyURL.host, 403 | let port = proxyURL.port { 404 | let proxyConfiguration: [AnyHashable: Any] = [ 405 | kCFNetworkProxiesHTTPEnable as String: true, 406 | kCFNetworkProxiesHTTPProxy as String: host, 407 | kCFNetworkProxiesHTTPPort as String: port, 408 | kCFNetworkProxiesHTTPSEnable as String: true, 409 | kCFNetworkProxiesHTTPSProxy as String: host, 410 | kCFNetworkProxiesHTTPSPort as String: port 411 | ] 412 | let schemeHandler = ProxiedSchemeHandler(proxyConfiguration: proxyConfiguration) 413 | config.setURLSchemeHandler(schemeHandler, forURLScheme: "proxied-http") 414 | config.setURLSchemeHandler(schemeHandler, forURLScheme: "proxied-https") 415 | } 416 | // ================================ 417 | 418 | // MARK: WebView Initialisation 419 | 420 | let webView = WKWebView(frame: .zero, configuration: config) 421 | Task { @MainActor in webViewReference = webView } 422 | 423 | // Store a weak reference in Coordinator to break potential cycles 424 | context.coordinator.webView = webView 425 | 426 | // Configure webview delegates 427 | webView.uiDelegate = context.coordinator 428 | webView.navigationDelegate = context.coordinator 429 | 430 | // Make background transparent 431 | webView.setValue(false, forKey: "drawsBackground") 432 | 433 | // Add message handlers 434 | // If these properties are added to, ensure you remove the handlers as well in `Coordinator` `deinit` 435 | webView.configuration.userContentController.add(context.coordinator, name: "channelClick") 436 | webView.configuration.userContentController.add(context.coordinator, name: "notify") 437 | webView.configuration.userContentController.add(context.coordinator, name: "notificationPermission") 438 | 439 | // MARK: Script Injection 440 | 441 | // Media Permissions Script 442 | webView.configuration.userContentController.addUserScript( 443 | WKUserScript( 444 | source: """ 445 | const originalGetUserMedia = navigator.mediaDevices.getUserMedia; 446 | navigator.mediaDevices.getUserMedia = async function(constraints) { 447 | console.log('getUserMedia requested with constraints:', constraints); 448 | return originalGetUserMedia.call(navigator.mediaDevices, constraints); 449 | }; 450 | 451 | const originalEnumerateDevices = navigator.mediaDevices.enumerateDevices; 452 | navigator.mediaDevices.enumerateDevices = async function() { 453 | console.log('enumerateDevices requested'); 454 | return originalEnumerateDevices.call(navigator.mediaDevices); 455 | }; 456 | """, 457 | injectionTime: .atDocumentEnd, 458 | forMainFrameOnly: true 459 | ) 460 | ) 461 | 462 | // Channel Click Handler Script 463 | webView.configuration.userContentController.addUserScript( 464 | WKUserScript( 465 | source: """ 466 | (function () { 467 | document.addEventListener('click', function(e) { 468 | const channel = e.target.closest('.blobContainer_a5ad63'); 469 | if (channel) { 470 | window.webkit.messageHandlers.channelClick.postMessage({type: 'channel'}); 471 | return; 472 | } 473 | 474 | const link = e.target.closest('.link_c91bad'); 475 | if (link) { 476 | e.preventDefault(); 477 | let href = link.getAttribute('href') || link.href || '/channels/@me'; 478 | if (href.startsWith('/')) { 479 | href = 'https://discord.com' + href; 480 | } 481 | console.log('Link clicked with href:', href); 482 | window.webkit.messageHandlers.channelClick.postMessage({type: 'user', url: href}); 483 | return; 484 | } 485 | 486 | const serverIcon = e.target.closest('.wrapper_f90abb'); 487 | if (serverIcon) { 488 | window.webkit.messageHandlers.channelClick.postMessage({type: 'server'}); 489 | } 490 | }); 491 | })(); 492 | """, 493 | injectionTime: .atDocumentEnd, 494 | forMainFrameOnly: true 495 | ) 496 | ) 497 | 498 | // Notification Handling Script 499 | webView.configuration.userContentController.addUserScript( 500 | WKUserScript( 501 | source: """ 502 | (function () { 503 | const Original = window.Notification; 504 | let perm = "default"; 505 | const map = new Map(); 506 | 507 | Object.defineProperty(Notification, "permission", { 508 | get: () => perm, 509 | configurable: true, 510 | }); 511 | 512 | class VoxaNotification extends Original { 513 | constructor(title, options = {}) { 514 | const id = crypto.randomUUID().toUpperCase(); 515 | super(title, options); 516 | this.notificationId = id; 517 | map.set(id, this); 518 | window.webkit?.messageHandlers?.notify?.postMessage({ 519 | title, 520 | options, 521 | notificationId: id, 522 | }); 523 | 524 | this.onshow = null; 525 | setTimeout(() => { 526 | this.dispatchEvent(new Event("show")); 527 | if (typeof this._onshow === "function") this._onshow(); 528 | }, 0); 529 | } 530 | 531 | close() { 532 | if (this.notificationId) { 533 | window.webkit?.messageHandlers?.closeNotification?.postMessage({ 534 | id: this.notificationId, 535 | }); 536 | } 537 | super.close(); 538 | } 539 | 540 | set onshow(h) { this._onshow = h; } 541 | get onshow() { return this._onshow; } 542 | 543 | set onerror(h) { this._onerror = h; } 544 | get onerror() { return this._onerror; } 545 | 546 | handleError(e) { 547 | if (typeof this._onerror === "function") this._onerror(e); 548 | } 549 | } 550 | 551 | window.Notification = VoxaNotification; 552 | 553 | Notification.requestPermission = function (cb) { 554 | return new Promise((resolve) => { 555 | window.webkit?.messageHandlers?.notificationPermission?.postMessage({}); 556 | window.notificationPermissionCallback = resolve; 557 | }).then((res) => { 558 | if (typeof cb === "function") cb(res); 559 | return res; 560 | }); 561 | }; 562 | 563 | window.addEventListener("nativePermissionResponse", (e) => { 564 | if (window.notificationPermissionCallback) { 565 | perm = e.detail.permission || "default"; 566 | window.notificationPermissionCallback(perm); 567 | window.notificationPermissionCallback = null; 568 | } 569 | }); 570 | 571 | window.addEventListener("notificationError", (e) => { 572 | const { notificationId, error } = e.detail; 573 | const n = map.get(notificationId); 574 | if (n) { 575 | n.handleError(error); 576 | map.delete(notificationId); 577 | } 578 | }); 579 | 580 | window.addEventListener("notificationSuccess", (e) => { 581 | const { notificationId } = e.detail; 582 | const n = map.get(notificationId); 583 | if (n) { 584 | console.log(`Notification successfully added: ${notificationId}`); 585 | map.delete(notificationId); 586 | } 587 | }); 588 | })(); 589 | """, 590 | injectionTime: .atDocumentStart, 591 | forMainFrameOnly: false 592 | ) 593 | ) 594 | 595 | rpcBridge.startBridge(for: webView) 596 | 597 | loadPluginsAndCSS(webView: webView) 598 | // ===== PROXY SUPPORT ADDED ===== 599 | var loadURL = initialURL 600 | if useDiscordProxy { 601 | var components = URLComponents(url: initialURL, resolvingAgainstBaseURL: false) 602 | if let scheme = components?.scheme?.lowercased() { 603 | if scheme == "http" { 604 | components?.scheme = "proxied-http" 605 | } else if scheme == "https" { 606 | components?.scheme = "proxied-https" 607 | } 608 | } 609 | if let newURL = components?.url { 610 | loadURL = newURL 611 | } 612 | } 613 | // ================================ 614 | webView.load(URLRequest(url: loadURL)) 615 | 616 | return webView 617 | } 618 | 619 | func updateNSView(_ nsView: WKWebView, context: Context) { 620 | // If you wish to update the webView here (e.g., reload or inject new CSS), 621 | // you can do so. Currently, no updates are necessary. 622 | loadPluginsAndCSS(webView: nsView) 623 | } 624 | 625 | // MARK: - Coordinator 626 | 627 | class Coordinator: NSObject, WKScriptMessageHandler, WKUIDelegate, WKNavigationDelegate { 628 | weak var webView: WKWebView? 629 | var parent: WebView 630 | 631 | init(_ parent: WebView) { 632 | self.parent = parent 633 | } 634 | 635 | deinit { 636 | // avoid memory leaks 637 | webView?.configuration.userContentController.removeScriptMessageHandler(forName: "channelClick") 638 | webView?.configuration.userContentController.removeScriptMessageHandler(forName: "notify") 639 | webView?.configuration.userContentController.removeScriptMessageHandler(forName: "notificationPermission") 640 | } 641 | 642 | // MARK: - WKWebView Delegate Methods 643 | 644 | @available(macOS 12.0, *) 645 | func webView( 646 | _ webView: WKWebView, 647 | requestMediaCapturePermissionFor origin: WKSecurityOrigin, 648 | initiatedByFrame frame: WKFrameInfo, 649 | type: WKMediaCaptureType, 650 | decisionHandler: @escaping (WKPermissionDecision) -> Void 651 | ) { 652 | print("Requesting permission for media type:", type) 653 | decisionHandler(.grant) 654 | } 655 | 656 | func webView( 657 | _ webView: WKWebView, 658 | runOpenPanelWith parameters: WKOpenPanelParameters, 659 | initiatedByFrame frame: WKFrameInfo, 660 | completionHandler: @escaping ([URL]?) -> Void 661 | ) { 662 | let openPanel = NSOpenPanel() 663 | openPanel.canChooseFiles = true 664 | openPanel.canChooseDirectories = false 665 | openPanel.allowsMultipleSelection = parameters.allowsMultipleSelection 666 | 667 | openPanel.begin { response in 668 | if response == .OK { 669 | completionHandler(openPanel.urls) 670 | } else { 671 | completionHandler(nil) 672 | } 673 | } 674 | } 675 | 676 | func webView( 677 | _ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, 678 | decisionHandler: @escaping (WKNavigationActionPolicy) -> Void 679 | ) { 680 | if let url = navigationAction.request.url, 681 | navigationAction.navigationType == .linkActivated 682 | { 683 | NSWorkspace.shared.open(url) 684 | decisionHandler(.cancel) 685 | } else { 686 | decisionHandler(.allow) 687 | } 688 | } 689 | 690 | func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { 691 | loadPluginsAndCSS(webView: webView) 692 | } 693 | 694 | // MARK: - Script Message Handling 695 | 696 | func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { 697 | switch message.name { 698 | case "notify": /// Notification payload is sent to webview 699 | guard 700 | let body = message.body as? [String: Any], 701 | let title = body["title"] as? String, 702 | let options = body["options"] as? [String: Any], 703 | let notificationId = body["notificationId"] as? String 704 | else { 705 | print("Received malformed notify message.") 706 | return 707 | } 708 | 709 | print("Received notify message: \(title) - \(options) - ID: \(notificationId)") 710 | 711 | let notification = UNMutableNotificationContent() 712 | notification.title = title 713 | notification.body = options["body"] as? String ?? "" 714 | 715 | if let soundName = options["sound"] as? String { 716 | notification.sound = UNNotificationSound(named: UNNotificationSoundName(soundName)) 717 | } else { 718 | notification.sound = .default 719 | } 720 | 721 | let request = UNNotificationRequest( 722 | identifier: notificationId, 723 | content: notification, 724 | trigger: nil 725 | ) 726 | 727 | UNUserNotificationCenter.current().add(request) { error in 728 | guard error == nil else { 729 | let error = error! 730 | print("Error adding notification: \(error.localizedDescription)") 731 | 732 | // Dispatch notification error event 733 | Task { @MainActor in 734 | do { 735 | try await self.webView?.evaluateJavaScript(""" 736 | window.dispatchEvent( 737 | new CustomEvent('notificationError', { 738 | detail: { 739 | notificationId: '\(notificationId)', 740 | error: '\(error.localizedDescription)' 741 | } 742 | }) 743 | ); 744 | """ 745 | ) 746 | print("Error response has additionally been dispatched to web content. (notificationId: \(notificationId))") 747 | } catch { 748 | print("Error evaluating notification error event JavaScript: \(error.localizedDescription)") 749 | } 750 | } 751 | return 752 | } 753 | 754 | print("Notification added: \(title) - ID: \(notificationId)") 755 | 756 | // Dispatch notification success event 757 | Task { @MainActor in 758 | do { 759 | try await self.webView?.evaluateJavaScript(""" 760 | window.dispatchEvent( 761 | new CustomEvent('notificationSuccess', { 762 | detail: { 763 | notificationId: '\(notificationId)' 764 | } 765 | }) 766 | ); 767 | """ 768 | ) 769 | print("Success response dispatched to web content for notification ID: \(notificationId)") 770 | } catch { 771 | print("Error evaluating notification success event JavaScript: \(error.localizedDescription)") 772 | } 773 | } 774 | } 775 | 776 | case "notificationPermission": /// Notification permission payload is sent to webview 777 | print("Received notificationPermission message") 778 | UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in 779 | let permission = granted ? "granted" : "denied" 780 | print("Notification permission \(permission)") 781 | 782 | // Dispatch permission response event 783 | Task { @MainActor in 784 | do { 785 | try await self.webView?.evaluateJavaScript(""" 786 | window.dispatchEvent( 787 | new CustomEvent('nativePermissionResponse', { 788 | detail: { 789 | permission: '\(permission)' 790 | } 791 | }) 792 | ); 793 | """ 794 | ) 795 | print("Permission response dispatched to web content") 796 | } catch { 797 | print("Error evaluating permission response event JavaScript: \(error.localizedDescription)") 798 | } 799 | } 800 | } 801 | 802 | default: 803 | print("Unimplemented message: \(message.name)") 804 | } 805 | } 806 | } 807 | } 808 | 809 | 810 | /// Performs a hard reload of the WebView by clearing scripts and reloading the initial URL 811 | func hardReloadWebView(webView: WKWebView) { 812 | webView.configuration.userContentController.removeAllUserScripts() 813 | loadPluginsAndCSS(webView: webView) 814 | let releaseChannel = UserDefaults.standard.string(forKey: "discordReleaseChannel") ?? "" 815 | let url = DiscordReleaseChannel(rawValue: releaseChannel)?.url ?? DiscordReleaseChannel.stable.url 816 | 817 | webView.load(URLRequest(url: url)) 818 | } 819 | 820 | /// ===== PROXY SUPPORT: Custom URL Scheme Handler ===== 821 | class ProxiedSchemeHandler: NSObject, WKURLSchemeHandler { 822 | let proxyConfiguration: [AnyHashable: Any] 823 | 824 | init(proxyConfiguration: [AnyHashable: Any]) { 825 | self.proxyConfiguration = proxyConfiguration 826 | } 827 | 828 | func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { 829 | guard let url = urlSchemeTask.request.url else { 830 | urlSchemeTask.didFailWithError(NSError(domain: "InvalidURL", code: -1, userInfo: nil)) 831 | return 832 | } 833 | var components = URLComponents(url: url, resolvingAgainstBaseURL: false) 834 | if components?.scheme == "proxied-http" { 835 | components?.scheme = "http" 836 | } else if components?.scheme == "proxied-https" { 837 | components?.scheme = "https" 838 | } 839 | guard let realURL = components?.url else { 840 | urlSchemeTask.didFailWithError(NSError(domain: "InvalidConvertedURL", code: -1, userInfo: nil)) 841 | return 842 | } 843 | var newRequest = URLRequest(url: realURL) 844 | newRequest.allHTTPHeaderFields = urlSchemeTask.request.allHTTPHeaderFields 845 | 846 | let config = URLSessionConfiguration.default 847 | config.connectionProxyDictionary = proxyConfiguration 848 | let session = URLSession(configuration: config) 849 | let task = session.dataTask(with: newRequest) { data, response, error in 850 | if let error = error { 851 | urlSchemeTask.didFailWithError(error) 852 | return 853 | } 854 | if let response = response, let data = data { 855 | urlSchemeTask.didReceive(response) 856 | urlSchemeTask.didReceive(data) 857 | urlSchemeTask.didFinish() 858 | } 859 | } 860 | task.resume() 861 | } 862 | 863 | func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) { 864 | // Handle cancellation if needed. 865 | } 866 | } 867 | --------------------------------------------------------------------------------