├── .github ├── FUNDING.yml ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── pages-deploy.yml │ └── tauri.yml ├── src-tauri ├── version.json ├── build.rs ├── gen │ ├── apple │ │ ├── .gitignore │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── AppIcon-512@2x.png │ │ │ │ ├── AppIcon-20x20@1x.png │ │ │ │ ├── AppIcon-20x20@2x.png │ │ │ │ ├── AppIcon-20x20@3x.png │ │ │ │ ├── AppIcon-29x29@1x.png │ │ │ │ ├── AppIcon-29x29@2x.png │ │ │ │ ├── AppIcon-29x29@3x.png │ │ │ │ ├── AppIcon-40x40@1x.png │ │ │ │ ├── AppIcon-40x40@2x.png │ │ │ │ ├── AppIcon-40x40@3x.png │ │ │ │ ├── AppIcon-60x60@2x.png │ │ │ │ ├── AppIcon-60x60@3x.png │ │ │ │ ├── AppIcon-76x76@1x.png │ │ │ │ ├── AppIcon-76x76@2x.png │ │ │ │ ├── AppIcon-20x20@2x-1.png │ │ │ │ ├── AppIcon-29x29@2x-1.png │ │ │ │ ├── AppIcon-40x40@2x-1.png │ │ │ │ └── AppIcon-83.5x83.5@2x.png │ │ ├── Sources │ │ │ └── app │ │ │ │ ├── main.mm │ │ │ │ └── bindings │ │ │ │ └── bindings.h │ │ ├── app.xcodeproj │ │ │ └── project.xcworkspace │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata │ │ │ │ └── WorkspaceSettings.xcsettings │ │ ├── app_iOS │ │ │ ├── app_iOS.entitlements │ │ │ └── Info.plist │ │ ├── ExportOptions.plist │ │ └── Podfile │ ├── android │ │ ├── .idea │ │ │ ├── .gitignore │ │ │ ├── compiler.xml │ │ │ ├── kotlinc.xml │ │ │ ├── vcs.xml │ │ │ ├── discord.xml │ │ │ ├── deploymentTargetDropDown.xml │ │ │ ├── migrations.xml │ │ │ ├── misc.xml │ │ │ └── gradle.xml │ │ ├── settings.gradle │ │ ├── app │ │ │ ├── src │ │ │ │ └── main │ │ │ │ │ ├── java │ │ │ │ │ └── chat │ │ │ │ │ │ └── spacebar │ │ │ │ │ │ └── app │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ │ ├── values │ │ │ │ │ │ ├── strings.xml │ │ │ │ │ │ ├── themes.xml │ │ │ │ │ │ └── colors.xml │ │ │ │ │ ├── xml │ │ │ │ │ │ └── file_paths.xml │ │ │ │ │ ├── values-night │ │ │ │ │ │ └── themes.xml │ │ │ │ │ ├── layout │ │ │ │ │ │ └── activity_main.xml │ │ │ │ │ └── drawable-v24 │ │ │ │ │ │ └── ic_launcher_foreground.xml │ │ │ │ │ └── AndroidManifest.xml │ │ │ ├── .gitignore │ │ │ ├── proguard-rules.pro │ │ │ └── build.gradle.kts │ │ ├── gradle │ │ │ └── wrapper │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ └── gradle-wrapper.properties │ │ ├── .editorconfig │ │ ├── .gitignore │ │ ├── buildSrc │ │ │ ├── build.gradle.kts │ │ │ └── src │ │ │ │ └── main │ │ │ │ └── java │ │ │ │ └── chat │ │ │ │ └── spacebar │ │ │ │ └── app │ │ │ │ └── kotlin │ │ │ │ └── BuildTask.kt │ │ ├── build.gradle.kts │ │ └── gradle.properties │ └── schemas │ │ └── capabilities.json ├── icons │ ├── icon.ico │ ├── icon.png │ ├── 32x32.png │ ├── icon.icns │ ├── 128x128.png │ ├── StoreLogo.png │ ├── sidebar.bmp │ ├── 128x128@2x.png │ ├── Square30x30Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square310x310Logo.png │ ├── ios │ │ ├── AppIcon-512@2x.png │ │ ├── AppIcon-20x20@1x.png │ │ ├── AppIcon-20x20@2x.png │ │ ├── AppIcon-20x20@3x.png │ │ ├── AppIcon-29x29@1x.png │ │ ├── AppIcon-29x29@2x.png │ │ ├── AppIcon-29x29@3x.png │ │ ├── AppIcon-40x40@1x.png │ │ ├── AppIcon-40x40@2x.png │ │ ├── AppIcon-40x40@3x.png │ │ ├── AppIcon-60x60@2x.png │ │ ├── AppIcon-60x60@3x.png │ │ ├── AppIcon-76x76@1x.png │ │ ├── AppIcon-76x76@2x.png │ │ ├── AppIcon-20x20@2x-1.png │ │ ├── AppIcon-29x29@2x-1.png │ │ ├── AppIcon-40x40@2x-1.png │ │ └── AppIcon-83.5x83.5@2x.png │ └── android │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ └── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png ├── .gitignore ├── .idea │ ├── vcs.xml │ ├── .gitignore │ ├── modules.xml │ ├── src-tauri.iml │ └── discord.xml ├── src │ ├── main.rs │ └── tray.rs ├── tauri.android.conf.json ├── tauri.ios.conf.json ├── tauri.linux.conf.json ├── tauri.macos.conf.json ├── tauri.windows.conf.json ├── capabilities │ └── base.json ├── Cargo.toml └── tauri.conf.json ├── README.md ├── .eslintignore ├── .vscode ├── settings.json └── extensions.json ├── app-icon.png ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json └── splashscreen.css ├── src ├── components │ ├── banners │ │ ├── index.ts │ │ └── OfflineBanner.tsx │ ├── modals │ │ ├── ForgotPasswordModal.tsx │ │ ├── SettingsPages │ │ │ └── DeveloperSettingsPage.tsx │ │ ├── ErrorModal.tsx │ │ ├── index.ts │ │ ├── InviteUnauthedModal.tsx │ │ ├── TextAttachmentViewerModal.tsx │ │ ├── AddServerModal.tsx │ │ ├── DeleteMessageModal.tsx │ │ ├── ImageViewerModal.tsx │ │ ├── InviteModal.tsx │ │ └── LeaveServerModal.tsx │ ├── Text.tsx │ ├── media │ │ ├── index.ts │ │ ├── File.tsx │ │ └── Audio.tsx │ ├── markdown │ │ ├── components │ │ │ ├── index.ts │ │ │ ├── Timestamp.tsx │ │ │ ├── Spoiler.tsx │ │ │ └── Codeblock.tsx │ │ ├── index.ts │ │ └── Markdown.tsx │ ├── Container.tsx │ ├── SectionHeader.tsx │ ├── floating │ │ ├── index.ts │ │ ├── FloatingContent.tsx │ │ └── FloatingTrigger.tsx │ ├── contextMenus │ │ ├── index.ts │ │ ├── ChannelMentionContextMenu.tsx │ │ ├── MessageContextMenu.tsx │ │ ├── ChannelContextMenu.tsx │ │ ├── ContextMenu.tsx │ │ └── GuildContextMenu.tsx │ ├── Divider.tsx │ ├── Icon.tsx │ ├── SectionTitle.tsx │ ├── Loader.tsx │ ├── FormComponents.tsx │ ├── Tooltip.tsx │ ├── common │ │ └── animations.ts │ ├── captcha │ │ └── HCaptchaModal.tsx │ ├── styles.module.css │ ├── EmojiRenderer.tsx │ ├── Link.tsx │ ├── AuthenticationGuard.tsx │ ├── messaging │ │ ├── MessageGroup.tsx │ │ ├── MessageTextArea.tsx │ │ └── attachments │ │ │ └── AttachmentUploadProgress.tsx │ ├── SidebarPill.tsx │ ├── ChannelSidebar.tsx │ ├── ListSection.tsx │ ├── index.ts │ ├── HCaptcha.tsx │ ├── ErrorBoundary.tsx │ ├── MemberList │ │ └── MemberList.tsx │ ├── IconButton.tsx │ ├── ChannelHeader.tsx │ └── ChannelList │ │ └── ChannelList.tsx ├── vite-env.d.ts ├── controllers │ ├── banners │ │ ├── index.ts │ │ ├── BannerRenderer.tsx │ │ └── types.ts │ └── modals │ │ ├── index.ts │ │ ├── ModalRenderer.tsx │ │ └── types.ts ├── hooks │ ├── useLogger.ts │ ├── useAppStore.ts │ ├── useFloatingContext.tsx │ ├── useWindowResize.ts │ ├── useFloating.tsx │ └── useInstanceValidation.ts ├── contexts │ ├── FloatingContext.tsx │ ├── ContextMenuContext.ts │ └── ContextMenuContextProvider.tsx ├── utils │ ├── revison.ts │ ├── mui │ │ ├── ownerDocument.ts │ │ ├── ownerWindow.ts │ │ ├── index.ts │ │ ├── useEnhancedEffect.ts │ │ ├── debounce.ts │ │ ├── setRef.ts │ │ └── useForkRef.ts │ ├── interfaces │ │ └── common.ts │ ├── i18n.ts │ ├── index.ts │ ├── debounce.ts │ ├── Globals.ts │ ├── Logger.ts │ ├── emojiParser.ts │ └── constants.ts ├── pages │ ├── NotFound.tsx │ ├── LogoutPage.tsx │ ├── AppPage.tsx │ ├── ErrorPage.tsx │ ├── LoadingPage.tsx │ └── InvitePage.tsx ├── stores │ ├── objects │ │ ├── index.ts │ │ ├── ReadState.ts │ │ ├── Emoji.ts │ │ ├── Role.ts │ │ ├── Presence.ts │ │ ├── MessageBase.ts │ │ └── QueuedMessage.ts │ ├── EmojiStore.ts │ ├── ThemeStore.ts │ ├── index.ts │ ├── PrivateChannelStore.ts │ ├── GuildStore.ts │ ├── RoleStore.ts │ ├── MessageQueue.ts │ ├── ReadStateStore.ts │ ├── PresenceStore.ts │ └── UserStore.ts ├── index.css ├── custom.d.ts ├── assets │ └── images │ │ └── logo │ │ ├── Spacebar_Icon.svg │ │ └── icon-rounded.svg └── index.tsx ├── .prettierignore ├── nix-build-test.sh ├── tsconfig.node.json ├── .prettierrc ├── nix-rebuild-flake.sh ├── .gitignore ├── .eslintrc ├── .editorconfig ├── scripts └── tauri-version.js ├── notice └── index.html ├── flake.template.nix ├── flake.nix ├── tsconfig.json ├── flake.lock └── index.html /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: spacebar -------------------------------------------------------------------------------- /src-tauri/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.2+00" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spacebar Client 2 | 3 | This is a WIP React app 4 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build(); 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/gen/apple/.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata/ 2 | build/ 3 | Externals/ 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | README.md 4 | LICENSE 5 | public 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "references.preferredLocation": "view" 3 | } 4 | -------------------------------------------------------------------------------- /app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/app-icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src-tauri/gen/android/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /src/components/banners/index.ts: -------------------------------------------------------------------------------- 1 | export { default as OfflineBanner } from "./OfflineBanner"; 2 | -------------------------------------------------------------------------------- /src-tauri/gen/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | apply from: 'tauri.settings.gradle' 4 | -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/sidebar.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/sidebar.bmp -------------------------------------------------------------------------------- /src/components/modals/ForgotPasswordModal.tsx: -------------------------------------------------------------------------------- 1 | export function ForgotPasswordModal() { 2 | return null; 3 | } 4 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | public 2 | dist 3 | node_modules 4 | .github 5 | .vscode 6 | src-tauri/target 7 | src-tauri/gen 8 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "version": 1, 4 | "author": "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/ios/AppIcon-512@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/ios/AppIcon-20x20@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/ios/AppIcon-20x20@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/ios/AppIcon-20x20@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/ios/AppIcon-29x29@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/ios/AppIcon-29x29@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/ios/AppIcon-29x29@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/ios/AppIcon-40x40@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/ios/AppIcon-40x40@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/ios/AppIcon-40x40@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/ios/AppIcon-60x60@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/ios/AppIcon-60x60@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/ios/AppIcon-76x76@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/ios/AppIcon-76x76@2x.png -------------------------------------------------------------------------------- /src/components/Text.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export default styled.div` 4 | color: var(--text); 5 | `; 6 | -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/ios/AppIcon-20x20@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/ios/AppIcon-29x29@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/ios/AppIcon-40x40@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png -------------------------------------------------------------------------------- /src/components/media/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Audio"; 2 | export * from "./File"; 3 | export * from "./Video"; 4 | export * from "./Text"; 5 | -------------------------------------------------------------------------------- /src/controllers/banners/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./BannerController"; 2 | export * from "./BannerRenderer"; 3 | export * from "./types"; 4 | -------------------------------------------------------------------------------- /src/hooks/useLogger.ts: -------------------------------------------------------------------------------- 1 | import Logger from "@utils/Logger"; 2 | 3 | export default function (name: string) { 4 | return new Logger(name); 5 | } 6 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/java/chat/spacebar/app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package chat.spacebar.app 2 | 3 | class MainActivity : TauriActivity() 4 | -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Sources/app/main.mm: -------------------------------------------------------------------------------- 1 | #include "bindings/bindings.h" 2 | 3 | int main(int argc, char * argv[]) { 4 | ffi::start_app(); 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/controllers/modals/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ModalController"; 2 | export { default as ModalRenderer } from "./ModalRenderer"; 3 | export * from "./types"; 4 | -------------------------------------------------------------------------------- /src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src-tauri/gen/apple/Sources/app/bindings/bindings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace ffi { 4 | extern "C" { 5 | void start_app(); 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src/components/markdown/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Codeblock"; 2 | export * from "./Mention"; 3 | export * from "./Spoiler"; 4 | export * from "./Timestamp"; 5 | -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src/hooks/useAppStore.ts: -------------------------------------------------------------------------------- 1 | import AppStore from "@stores/AppStore"; 2 | 3 | export const appStore = new AppStore(); 4 | 5 | export function useAppStore() { 6 | return appStore; 7 | } 8 | -------------------------------------------------------------------------------- /nix-build-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | #nix build --update-input pnpm2nix --debugger --ignore-try 3 | nix build --debugger --ignore-try --print-out-paths --print-build-logs --http2 "$@" 4 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/.gitignore: -------------------------------------------------------------------------------- 1 | /src/main/java/chat/spacebar/app/generated 2 | /src/main/jniLibs/**/*.so 3 | /src/main/assets/tauri.conf.json 4 | /tauri.build.gradle.kts 5 | /proguard-tauri.pro -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Spacebar 3 | Spacebar 4 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png -------------------------------------------------------------------------------- /src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacebarchat/client/HEAD/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | spacebar::run(); 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/gen/android/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src-tauri/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /src-tauri/gen/android/.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /src/components/Container.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export default styled.div` 4 | background-color: var(--background-tertiary); 5 | color: var(--text); 6 | overflow: hidden; 7 | display: flex; 8 | `; 9 | -------------------------------------------------------------------------------- /src-tauri/gen/apple/app.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/markdown/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components"; 2 | export { default as Markdown } from "./Markdown"; 3 | export type { MarkdownProps } from "./Markdown"; 4 | export { default as MarkdownRenderer } from "./MarkdownRenderer"; 5 | -------------------------------------------------------------------------------- /src-tauri/gen/android/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/controllers/banners/BannerRenderer.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from "mobx-react-lite"; 2 | import { bannerController } from "./BannerController"; 3 | 4 | export default observer(() => { 5 | return <>{bannerController.rendered}; 6 | }); 7 | -------------------------------------------------------------------------------- /src/controllers/banners/types.ts: -------------------------------------------------------------------------------- 1 | export type Banner = { 2 | key?: string; 3 | } & { 4 | type: "offline"; 5 | }; 6 | 7 | export type BannerProps = Banner & { type: T } & { 8 | onClose: () => void; 9 | }; 10 | -------------------------------------------------------------------------------- /src-tauri/gen/apple/app_iOS/app_iOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src-tauri/tauri.android.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "windows": [ 4 | { 5 | "fullscreen": false, 6 | "resizable": true, 7 | "title": "Spacebar", 8 | "maximized": true, 9 | "label": "main" 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/contexts/FloatingContext.tsx: -------------------------------------------------------------------------------- 1 | import useFloating from "@hooks/useFloating"; 2 | import React from "react"; 3 | 4 | type ContextType = ReturnType | null; 5 | 6 | export const FloatingContext = React.createContext(null); 7 | -------------------------------------------------------------------------------- /src/utils/revison.ts: -------------------------------------------------------------------------------- 1 | export const REPO_URL: string = "https://github.com/spacebarchat/client"; 2 | export const GIT_REVISION: string = "__GIT_REVISION__"; 3 | export const GIT_BRANCH: string = "__GIT_BRANCH__"; 4 | export const APP_VERSION = "__APP_VERSION__"; 5 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/mui/ownerDocument.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/mui/material-ui/blob/master/packages/mui-utils/src/ownerDocument/ownerDocument.ts 2 | export default function ownerDocument(node: Node | null | undefined): Document { 3 | return (node && node.ownerDocument) || document; 4 | } 5 | -------------------------------------------------------------------------------- /src-tauri/gen/android/.idea/discord.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "arrowParens": "always", 6 | "bracketSameLine": false, 7 | "bracketSpacing": true, 8 | "quoteProps": "as-needed", 9 | "useTabs": true, 10 | "singleQuote": false, 11 | "printWidth": 120 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import Container from "@components/Container"; 2 | import Text from "@components/Text"; 3 | 4 | function NotFoundPage() { 5 | return ( 6 | 7 | NotFound 8 | 9 | ); 10 | } 11 | 12 | export default NotFoundPage; 13 | -------------------------------------------------------------------------------- /src-tauri/gen/android/.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue May 10 19:22:52 CST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /src-tauri/gen/apple/ExportOptions.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | method 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /src-tauri/gen/android/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false -------------------------------------------------------------------------------- /src-tauri/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "." 5 | schedule: 6 | interval: "weekly" 7 | target-branch: "dev" 8 | - package-ecosystem: "cargo" 9 | directory: "src-tauri" 10 | schedule: 11 | interval: "weekly" 12 | target-branch: "dev" 13 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /src/pages/LogoutPage.tsx: -------------------------------------------------------------------------------- 1 | import { useAppStore } from "@hooks/useAppStore"; 2 | import React from "react"; 3 | 4 | function LogoutPage() { 5 | const app = useAppStore(); 6 | 7 | React.useEffect(() => { 8 | app.logout(); 9 | }, []); 10 | 11 | return
LogoutPage
; 12 | } 13 | 14 | export default LogoutPage; 15 | -------------------------------------------------------------------------------- /src-tauri/gen/android/.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /src/utils/mui/ownerWindow.ts: -------------------------------------------------------------------------------- 1 | import ownerDocument from "./ownerDocument"; 2 | 3 | // https://github.com/mui/material-ui/blob/master/packages/mui-utils/src/ownerWindow/ownerWindow.ts 4 | export default function ownerWindow(node: Node | undefined): Window { 5 | const doc = ownerDocument(node); 6 | return doc.defaultView || window; 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/AppPage.tsx: -------------------------------------------------------------------------------- 1 | import Loader from "@components/Loader"; 2 | import { observer } from "mobx-react-lite"; 3 | import { Navigate } from "react-router-dom"; 4 | 5 | function AppPage() { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export default observer(AppPage); 14 | -------------------------------------------------------------------------------- /src-tauri/tauri.ios.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "bundle": { 3 | "iOS": { 4 | "developmentTeam": "47RXBB8X9K" 5 | } 6 | }, 7 | "app": { 8 | "windows": [ 9 | { 10 | "fullscreen": false, 11 | "resizable": true, 12 | "title": "Spacebar", 13 | "maximized": true, 14 | "label": "main" 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/SectionHeader.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const SectionHeader = styled.div` 4 | display: flex; 5 | padding: 12px 16px; 6 | margin-bottom: 1px; 7 | box-shadow: 0 1px 0 hsl(0deg 0% 0% / 0.3); 8 | align-items: center; 9 | justify-content: space-between; 10 | white-space: nowrap; 11 | height: 50px; 12 | `; 13 | -------------------------------------------------------------------------------- /src/utils/interfaces/common.ts: -------------------------------------------------------------------------------- 1 | export type OneKeyFrom< 2 | T, 3 | M = object, 4 | K extends keyof T = keyof T, 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | > = K extends any 7 | ? M & Pick, K> & Partial, never>> extends infer O 8 | ? { [P in keyof O]: O[P] } 9 | : never 10 | : never; 11 | -------------------------------------------------------------------------------- /nix-rebuild-flake.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nix-shell 2 | #!nix-shell -i "bash -x" -p nodejs nodePackages.ts-node prefetch-npm-deps bash 3 | rm -rfv package-lock.json 4 | npm i --save --ignore-scripts 5 | DEPS_HASH=`prefetch-npm-deps package-lock.json` 6 | sed 's/$NPM_HASH/'${DEPS_HASH/\//\\\/}'/g' flake.template.nix > flake.nix 7 | #sha256-5iurI8d2mael4qR/gs4IeZpf5d6+QPBoZ+kvhrwoOIU= 8 | -------------------------------------------------------------------------------- /src/hooks/useFloatingContext.tsx: -------------------------------------------------------------------------------- 1 | import { FloatingContext } from "@contexts/FloatingContext"; 2 | import React from "react"; 3 | 4 | export default () => { 5 | const context = React.useContext(FloatingContext); 6 | 7 | if (context == null) { 8 | throw new Error("Floating components must be wrapped in "); 9 | } 10 | 11 | return context; 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/mui/index.ts: -------------------------------------------------------------------------------- 1 | export { default as muiDebounce } from "./debounce"; 2 | export { default as ownerDocument } from "./ownerDocument"; 3 | export { default as ownerWindow } from "./ownerWindow"; 4 | export { default as setRef } from "./setRef"; 5 | export { default as useEnhancedEffect } from "./useEnhancedEffect"; 6 | export { default as useForkRef } from "./useForkRef"; 7 | -------------------------------------------------------------------------------- /src-tauri/gen/android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | key.properties 17 | 18 | /.tauri 19 | /tauri.settings.gradle -------------------------------------------------------------------------------- /src/components/floating/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Floating"; 2 | export { default as Floating } from "./Floating"; 3 | export { default as FloatingContent } from "./FloatingContent"; 4 | export { default as FloatingTrigger } from "./FloatingTrigger"; 5 | export { default as GuildMenuPopout } from "./GuildMenuPopout"; 6 | export { default as UserProfilePopout } from "./UserProfilePopout"; 7 | -------------------------------------------------------------------------------- /src/pages/ErrorPage.tsx: -------------------------------------------------------------------------------- 1 | import Container from "@components/Container"; 2 | import Text from "@components/Text"; 3 | 4 | interface Props { 5 | error: Error; 6 | } 7 | 8 | function ErrorPage({ error }: Props) { 9 | return ( 10 | 11 | Oops, Something went wrong! 12 |
{error.message}
13 |
14 | ); 15 | } 16 | 17 | export default ErrorPage; 18 | -------------------------------------------------------------------------------- /src/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | export const calendarStrings = { 2 | sameDay: "[Today at] h:mm A", // The same day (Today at 2:30 AM) 3 | nextDay: "[Tomorrow at] h:mm A", // The next day (Tomorrow at 2:30 AM) 4 | lastDay: "[Yesterday at] h:mm A", // The day before (Yesterday at 2:30 AM) 5 | lastWeek: "[Last] dddd [at] h:mm A", // Last week (Last Monday at 2:30 AM) 6 | sameElse: "MM/DD/YYYY h:mm A", // Everything else (01/19/2018 2:30 AM) 7 | }; 8 | -------------------------------------------------------------------------------- /src/components/contextMenus/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ChannelContextMenu } from "./ChannelContextMenu"; 2 | export { default as ChannelMentionContextMenu } from "./ChannelMentionContextMenu"; 3 | export * from "./ContextMenu"; 4 | export { default as GuildContextMenu } from "./GuildContextMenu"; 5 | export { default as MessageContextMenu } from "./MessageContextMenu"; 6 | export { default as UserContextMenu } from "./UserContextMenu"; 7 | -------------------------------------------------------------------------------- /src-tauri/gen/apple/app.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Original 7 | DisableBuildSystemDeprecationDiagnostic 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src-tauri/gen/android/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # production 12 | dist 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | src-tauri/.cargo/config.toml 25 | 26 | .swc/ 27 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /src-tauri/tauri.linux.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "windows": [ 4 | { 5 | "width": 400, 6 | "height": 200, 7 | "decorations": false, 8 | "center": true, 9 | "url": "splashscreen.html", 10 | "label": "splashscreen" 11 | }, 12 | { 13 | "fullscreen": false, 14 | "resizable": true, 15 | "title": "Spacebar", 16 | "maximized": true, 17 | "visible": false, 18 | "label": "main" 19 | } 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src-tauri/tauri.macos.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "windows": [ 4 | { 5 | "width": 400, 6 | "height": 200, 7 | "decorations": false, 8 | "center": true, 9 | "url": "splashscreen.html", 10 | "label": "splashscreen" 11 | }, 12 | { 13 | "fullscreen": false, 14 | "resizable": true, 15 | "title": "Spacebar", 16 | "maximized": true, 17 | "visible": false, 18 | "label": "main" 19 | } 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src-tauri/tauri.windows.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "windows": [ 4 | { 5 | "width": 400, 6 | "height": 200, 7 | "decorations": false, 8 | "center": true, 9 | "url": "splashscreen.html", 10 | "label": "splashscreen" 11 | }, 12 | { 13 | "fullscreen": false, 14 | "resizable": true, 15 | "title": "Spacebar", 16 | "maximized": true, 17 | "visible": false, 18 | "label": "main" 19 | } 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Divider.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const HorizontalDivider = styled.div<{ nomargin?: boolean }>` 4 | margin-top: ${(props) => (props.nomargin ? "0" : "8px")}; 5 | z-index: 1; 6 | height: 1px; 7 | background-color: var(--text-disabled); 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | box-sizing: border-box; 12 | `; 13 | 14 | export const TextDivider = styled.span` 15 | padding: 0 4px; 16 | `; 17 | -------------------------------------------------------------------------------- /src-tauri/.idea/src-tauri.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src-tauri/gen/android/buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | gradlePlugin { 6 | plugins { 7 | create("pluginsForCoolKids") { 8 | id = "rust" 9 | implementationClass = "RustPlugin" 10 | } 11 | } 12 | } 13 | 14 | repositories { 15 | google() 16 | mavenCentral() 17 | } 18 | 19 | dependencies { 20 | compileOnly(gradleApi()) 21 | implementation("com.android.tools.build:gradle:8.0.0") 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src-tauri/.idea/discord.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | -------------------------------------------------------------------------------- /src-tauri/gen/android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | dependencies { 7 | classpath("com.android.tools.build:gradle:8.0.0") 8 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21") 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | mavenCentral() 16 | } 17 | } 18 | 19 | tasks.register("clean").configure { 20 | delete("build") 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src-tauri/gen/schemas/capabilities.json: -------------------------------------------------------------------------------- 1 | {"base":{"identifier":"base","description":"base","local":true,"windows":["main","splashscreen"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","updater:default","notification:default","os:allow-platform","os:allow-arch","os:allow-family","os:allow-locale","os:allow-os-type","os:allow-version","core:webview:allow-internal-toggle-devtools"],"platforms":["linux","macOS","windows","android","iOS"]}} -------------------------------------------------------------------------------- /src/components/modals/SettingsPages/DeveloperSettingsPage.tsx: -------------------------------------------------------------------------------- 1 | import SectionTitle from "@components/SectionTitle"; 2 | import { observer } from "mobx-react-lite"; 3 | import styled from "styled-components"; 4 | 5 | const Content = styled.div` 6 | display: flex; 7 | flex-direction: column; 8 | `; 9 | 10 | function DeveloperSettingsPage() { 11 | return ( 12 |
13 | Developer Options 14 | 15 |
16 | ); 17 | } 18 | 19 | export default observer(DeveloperSettingsPage); 20 | -------------------------------------------------------------------------------- /src/components/markdown/Markdown.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense, lazy } from "react"; 2 | import { withErrorBoundary } from "react-use-error-boundary"; 3 | 4 | const Renderer = lazy(() => import("./MarkdownRenderer")); 5 | 6 | export interface MarkdownProps { 7 | content: string; 8 | } 9 | 10 | function Markdown(props: MarkdownProps) { 11 | if (!props.content) return null; 12 | 13 | return ( 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | export default withErrorBoundary(Markdown); 21 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Spacebar", 3 | "name": "Spacebar", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./BitField"; 2 | export * from "./constants"; 3 | export { default as debounce } from "./debounce"; 4 | export * from "./Globals"; 5 | export * from "./i18n"; 6 | export { default as Logger } from "./Logger"; 7 | export { default as useForkRef } from "./mui/useForkRef"; 8 | export * from "./Permissions"; 9 | export { default as REST } from "./REST"; 10 | export * from "./revison"; 11 | export { default as Snowflake } from "./Snowflake"; 12 | export * from "./Utils"; 13 | 14 | export * from "./interfaces/api"; 15 | export * from "./interfaces/common"; 16 | -------------------------------------------------------------------------------- /src/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/revoltchat/revite/blob/master/src/lib/debounce.ts#L5 2 | 3 | export default function debounce(cb: (...args: unknown[]) => void, duration: number) { 4 | // Store the timer variable. 5 | let timer: NodeJS.Timeout; 6 | // This function is given to React. 7 | return (...args: unknown[]) => { 8 | // Get rid of the old timer. 9 | clearTimeout(timer); 10 | // Set a new timer. 11 | timer = setTimeout(() => { 12 | // Instead calling the new function. 13 | // (with the newer data) 14 | cb(...args); 15 | }, duration); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/modals/ErrorModal.tsx: -------------------------------------------------------------------------------- 1 | import { ModalProps } from "@/controllers/modals/types"; 2 | import { Modal } from "./ModalComponents"; 3 | 4 | export function ErrorModal({ error, ...props }: ModalProps<"error">) { 5 | return ( 6 | true, 11 | confirmation: true, 12 | children: Dismiss, 13 | palette: "primary", 14 | disabled: !(props.recoverable ?? true), 15 | }, 16 | ]} 17 | nonDismissable={!(props.recoverable ?? true)} 18 | > 19 | {error} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/mui/useEnhancedEffect.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | /** 4 | * A version of `React.useLayoutEffect` that does not show a warning when server-side rendering. 5 | * This is useful for effects that are only needed for client-side rendering but not for SSR. 6 | * 7 | * Before you use this hook, make sure to read https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85 8 | * and confirm it doesn't apply to your use-case. 9 | */ 10 | const useEnhancedEffect = typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect; 11 | 12 | export default useEnhancedEffect; 13 | -------------------------------------------------------------------------------- /src/components/Icon.tsx: -------------------------------------------------------------------------------- 1 | import * as Icons from "@mdi/js"; 2 | import { Icon as MdiIcon } from "@mdi/react"; 3 | import { IconProps as IconBaseProps } from "@mdi/react/dist/IconProps"; 4 | 5 | export type IconType = keyof typeof Icons; 6 | 7 | export interface IconProps extends Omit { 8 | icon: IconType; 9 | } 10 | 11 | function Icon(props: IconProps) { 12 | const path = Icons[props.icon]; 13 | if (!path) throw new Error(`Invalid icon name ${props.icon}`); 14 | 15 | const { icon, ...propSpread } = props; 16 | return ; 17 | } 18 | 19 | export default Icon; 20 | -------------------------------------------------------------------------------- /src/components/banners/OfflineBanner.tsx: -------------------------------------------------------------------------------- 1 | import Icon from "@components/Icon"; 2 | import styled from "styled-components"; 3 | 4 | const Wrapper = styled.div` 5 | display: flex; 6 | flex-direction: row; 7 | flex: 1; 8 | justify-content: center; 9 | align-items: center; 10 | `; 11 | 12 | const Text = styled.span` 13 | padding: 10px; 14 | color: var(--warning); 15 | `; 16 | 17 | function OfflineBanner() { 18 | return ( 19 | 20 | You are offline 21 | 22 | 23 | ); 24 | } 25 | 26 | export default OfflineBanner; 27 | -------------------------------------------------------------------------------- /src/stores/objects/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Channel } from "./Channel"; 2 | export { default as Guild } from "./Guild"; 3 | export { default as GuildMember } from "./GuildMember"; 4 | export * from "./Message"; 5 | export { default as Message } from "./Message"; 6 | export { default as MessageBase } from "./MessageBase"; 7 | export { default as Presence } from "./Presence"; 8 | export * from "./QueuedMessage"; 9 | export { default as QueuedMessage, QueuedMessageStatus } from "./QueuedMessage"; 10 | export { default as ReadState } from "./ReadState"; 11 | export { default as Role } from "./Role"; 12 | export { default as User } from "./User"; 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Spacebar Documentation 4 | url: https://docs.spacebar.chat/ 5 | about: Need documentation and examples for the Spacebar? Head over to Spacebar's official documentation. 6 | - name: Discord's Developer Documentation 7 | url: https://discord.com/developers/docs/intro 8 | about: Need help with the Discord resources? Head here instead of asking on Spacebar! 9 | - name: Spacebar' Official Discord server 10 | url: https://discord.com/invite/Ms5Ev7S6bF 11 | about: Need help with the server? Talk with us in our official server. 12 | -------------------------------------------------------------------------------- /src/components/SectionTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Container = styled.div` 5 | display: flex; 6 | `; 7 | 8 | const Text = styled.h2` 9 | color: var(--text); 10 | margin-bottom: 20px; 11 | font-size: 20px; 12 | font-weight: var(--font-weight-medium); 13 | flex: 1; 14 | `; 15 | 16 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 17 | interface Props {} 18 | 19 | function SectionTitle({ children }: React.PropsWithChildren) { 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | } 26 | 27 | export default SectionTitle; 28 | -------------------------------------------------------------------------------- /src/components/modals/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./AddServerModal"; 2 | export * from "./BanMemberModal"; 3 | export * from "./CreateChannelModel"; 4 | export * from "./CreateInviteModal"; 5 | export * from "./CreateServerModal"; 6 | export * from "./DeleteMessageModal"; 7 | export * from "./ErrorModal"; 8 | export * from "./ForgotPasswordModal"; 9 | export * from "./ImageViewerModal"; 10 | export * from "./TextAttachmentViewerModal"; 11 | export * from "./InviteModal"; 12 | export * from "./InviteUnauthedModal"; 13 | export * from "./JoinServerModal"; 14 | export * from "./KickMemberModal"; 15 | export * from "./LeaveServerModal"; 16 | export * from "./SettingsModal"; 17 | -------------------------------------------------------------------------------- /src-tauri/gen/apple/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | 3 | target 'app_iOS' do 4 | platform :ios, '13.0' 5 | # Pods for app_iOS 6 | end 7 | 8 | target 'app_macOS' do 9 | platform :osx, '11.0' 10 | # Pods for app_macOS 11 | end 12 | 13 | # Delete the deployment target for iOS and macOS, causing it to be inherited from the Podfile 14 | post_install do |installer| 15 | installer.pods_project.targets.each do |target| 16 | target.build_configurations.each do |config| 17 | config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET' 18 | config.build_settings.delete 'MACOSX_DEPLOYMENT_TARGET' 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import { useAppStore } from "@hooks/useAppStore"; 2 | import LoadingPage from "@pages/LoadingPage"; 3 | import { invoke } from "@tauri-apps/api/core"; 4 | import { isTauri } from "@utils"; 5 | import { observer } from "mobx-react-lite"; 6 | import React from "react"; 7 | 8 | interface Props { 9 | children: React.ReactNode; 10 | } 11 | function Loader(props: Props) { 12 | const app = useAppStore(); 13 | 14 | if (!app.isReady) { 15 | return ; 16 | } 17 | 18 | // close tauri splashscreen 19 | if (isTauri) invoke("close_splashscreen"); 20 | 21 | return <>{props.children}; 22 | } 23 | 24 | export default observer(Loader); 25 | -------------------------------------------------------------------------------- /src-tauri/capabilities/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "base", 3 | "description": "base", 4 | "windows": ["main", "splashscreen"], 5 | "permissions": [ 6 | "core:path:default", 7 | "core:event:default", 8 | "core:window:default", 9 | "core:app:default", 10 | "core:resources:default", 11 | "core:menu:default", 12 | "core:tray:default", 13 | "updater:default", 14 | "notification:default", 15 | "os:allow-platform", 16 | "os:allow-arch", 17 | "os:allow-family", 18 | "os:allow-locale", 19 | "os:allow-os-type", 20 | "os:allow-version", 21 | "core:webview:allow-internal-toggle-devtools" 22 | ], 23 | "platforms": ["linux", "macOS", "windows", "android", "iOS"] 24 | } 25 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"], 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["react-refresh"], 5 | "root": true, 6 | "rules": { 7 | "no-mixed-spaces-and-tabs": "off", 8 | "@typescript-eslint/no-var-requires": "off", 9 | "@typescript-eslint/no-non-null-assertion": "off", 10 | "@typescript-eslint/no-unused-vars": "off", 11 | "react-hooks/exhaustive-deps": "off", 12 | "react-hooks/rules-of-hooks": "warn", 13 | "@typescript-eslint/ban-ts-comment": "warn" 14 | }, 15 | "env": { 16 | "browser": true, 17 | "es2020": true 18 | }, 19 | "ignorePatterns": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /src/components/FormComponents.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | // TODO: migrate some things from AuthComponents 4 | 5 | export const InputSelect = styled.select` 6 | background-color: var(--background-secondary); 7 | color: var(--text); 8 | outline: none; 9 | border: 1px solid transparent; 10 | padding: 8px; 11 | height: 42px; 12 | font-weight: var(--font-weight-medium); 13 | cursor: pointer; 14 | border-radius: 12px; 15 | width: 100%; 16 | `; 17 | 18 | export const InputSelectOption = styled.option` 19 | background-color: var(--background-secondary); 20 | color: var(--text); 21 | 22 | &:hover { 23 | background-color: var(--background-secondary-highlight); 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /src/components/modals/InviteUnauthedModal.tsx: -------------------------------------------------------------------------------- 1 | import { ModalProps } from "@/controllers/modals"; 2 | import styled from "styled-components"; 3 | import { Modal, ModalHeader, ModalHeaderText, ModalSubHeaderText } from "./ModalComponents"; 4 | 5 | const ActionWrapper = styled.div` 6 | display: flex; 7 | flex-direction: column; 8 | gap: 8px; 9 | `; 10 | 11 | export function InviteUnauthedModal({ inviteData, ...props }: ModalProps<"invite">) { 12 | return ( 13 | 14 | 15 | You've been invited to join 16 | {inviteData.guild?.name} 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "aaron-bond.better-comments", 8 | "naumovs.color-highlight", 9 | "usernamehw.errorlens", 10 | "esbenp.prettier-vscode", 11 | "rust-lang.rust-analyzer", 12 | "tauri-apps.tauri-vscode", 13 | "wayou.vscode-todo-highlight", 14 | ], 15 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 16 | "unwantedRecommendations": [ 17 | 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { FloatingProps } from "@components/floating"; 2 | import styled from "styled-components"; 3 | 4 | const Container = styled.div` 5 | background-color: var(--background-tertiary); 6 | line-height: 16px; 7 | box-sizing: border-box; 8 | font-size: 14px; 9 | padding: 8px 12px; 10 | overflow: hidden; 11 | text-overflow: ellipsis; 12 | white-space: nowrap; 13 | max-width: 250px; 14 | border-radius: 4px; 15 | color: var(--text); 16 | box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.5); 17 | `; 18 | 19 | function Tooltip(props: FloatingProps<"tooltip">) { 20 | if (!props) return null; 21 | return {props.content}; 22 | } 23 | 24 | export default Tooltip; 25 | -------------------------------------------------------------------------------- /src/utils/Globals.ts: -------------------------------------------------------------------------------- 1 | import Logger from "./Logger"; 2 | import { DefaultRouteSettings, RouteSettings } from "./constants"; 3 | 4 | const logger = new Logger("Globals"); 5 | 6 | export const Globals: { 7 | load: () => void; 8 | save: () => void; 9 | routeSettings: RouteSettings; 10 | } = { 11 | load: () => { 12 | logger.info("Initializing Globals"); 13 | const settings = localStorage.getItem("routeSettings"); 14 | 15 | if (!settings) { 16 | return; 17 | } 18 | 19 | Globals.routeSettings = JSON.parse(settings); 20 | logger.info("Loaded route settings from storage"); 21 | }, 22 | save: () => { 23 | localStorage.setItem("routeSettings", JSON.stringify(Globals.routeSettings)); 24 | }, 25 | routeSettings: DefaultRouteSettings, 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/common/animations.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/revoltchat/components/blob/master/src/components/common/animations.ts 2 | 3 | import { keyframes } from "styled-components"; 4 | 5 | export const animationFadeIn = keyframes` 6 | 0% {opacity: 0;} 7 | 70% {opacity: 0;} 8 | 100% {opacity: 1;} 9 | `; 10 | 11 | export const animationFadeOut = keyframes` 12 | 0% {opacity: 1;} 13 | 70% {opacity: 0;} 14 | 100% {opacity: 0;} 15 | `; 16 | 17 | export const animationZoomIn = keyframes` 18 | 0% {transform: scale(0.5);} 19 | 98% {transform: scale(1.01);} 20 | 100% {transform: scale(1);} 21 | `; 22 | 23 | export const animationZoomOut = keyframes` 24 | 0% {transform: scale(1);} 25 | 100% {transform: scale(0.5);} 26 | `; 27 | -------------------------------------------------------------------------------- /src/components/captcha/HCaptchaModal.tsx: -------------------------------------------------------------------------------- 1 | import HCaptchaLib from "@hcaptcha/react-hcaptcha"; 2 | import React from "react"; 3 | import styled from "styled-components"; 4 | 5 | interface Props { 6 | open: boolean; 7 | siteKey: string; 8 | onVerify: (token: string) => void; 9 | } 10 | 11 | const Wrapper = styled.form` 12 | position: absolute; 13 | top: 0; 14 | `; 15 | 16 | function HCaptchaModal({ open, siteKey, onVerify }: Props) { 17 | const ref = React.useRef(null); 18 | 19 | const onLoad = () => { 20 | ref.current?.execute(); 21 | }; 22 | 23 | return open ? ( 24 | 25 | 26 | 27 | ) : null; 28 | } 29 | 30 | export default HCaptchaModal; 31 | -------------------------------------------------------------------------------- /src/utils/mui/debounce.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | export interface Cancelable { 3 | clear(): void; 4 | } 5 | 6 | // Corresponds to 10 frames at 60 Hz. 7 | // A few bytes payload overhead when lodash/debounce is ~3 kB and debounce ~300 B. 8 | export default function debounce any>(func: T, wait = 166) { 9 | let timeout: ReturnType; 10 | function debounced(...args: Parameters) { 11 | const later = () => { 12 | // @ts-expect-error types 13 | func.apply(this, args); 14 | }; 15 | clearTimeout(timeout); 16 | timeout = setTimeout(later, wait); 17 | } 18 | 19 | debounced.clear = () => { 20 | clearTimeout(timeout); 21 | }; 22 | 23 | return debounced as T & Cancelable; 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/Logger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | export default class Logger { 4 | constructor(public readonly name: string) { 5 | this.name = name; 6 | } 7 | 8 | debug(...args: any[]) { 9 | console.debug(`%c${new Date().toLocaleTimeString()} | ${this.name} | DEBUG |`, `color: LimeGreen`, ...args); 10 | } 11 | 12 | info(...args: any[]) { 13 | console.info(`%c${new Date().toLocaleTimeString()} | ${this.name} | INFO |`, `color: DodgerBlue`, ...args); 14 | } 15 | 16 | warn(...args: any[]) { 17 | console.warn(`%c${new Date().toLocaleTimeString()} | ${this.name} | WARN |`, `color: Tomato`, ...args); 18 | } 19 | 20 | error(...args: any[]) { 21 | console.error(`%c${new Date().toLocaleTimeString()} | ${this.name} | ERROR |`, `color: Red`, ...args); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/styles.module.css: -------------------------------------------------------------------------------- 1 | .item { 2 | position: relative; 3 | /* width: 100%; */ 4 | /* height: 100%; */ 5 | pointer-events: auto; 6 | /* transform-origin: 50% 50% 0px; */ 7 | /* padding-left: 32px; */ 8 | /* padding-right: 32px; */ 9 | box-sizing: border-box; 10 | display: flex; 11 | flex: 1; 12 | /* align-items: center; */ 13 | /* text-align: center; */ 14 | /* border-radius: 5px; */ 15 | /* box-shadow: 0px 10px 10px -5px rgba(0, 0, 0, 0.2); */ 16 | -webkit-user-select: none; 17 | user-select: none; 18 | } 19 | 20 | .fg { 21 | cursor: -webkit-grab; 22 | position: absolute; 23 | height: 100%; 24 | width: 100%; 25 | display: grid; 26 | } 27 | 28 | .fg > * { 29 | pointer-events: none; 30 | } 31 | 32 | .container { 33 | display: flex; 34 | align-items: center; 35 | height: 100%; 36 | justify-content: center; 37 | } 38 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | # end_of_line = lf 6 | # indent_size = 4 7 | indent_style = tab 8 | insert_final_newline = true 9 | # max_line_length = 120 10 | # tab_width = 4 11 | 12 | [*.less] 13 | # indent_size = 2 14 | 15 | [*.sass] 16 | # indent_size = 2 17 | 18 | [*.scss] 19 | # indent_size = 2 20 | 21 | [*.vue] 22 | # indent_style = tabq 23 | 24 | [{*.ats,*.cts,*.mts,*.ts}] 25 | # indent_style = tab 26 | 27 | [{*.bash,*.sh,*.zsh}] 28 | # indent_size = 2 29 | # tab_width = 2 30 | 31 | [{*.cjs,*.js}] 32 | # indent_style = tab 33 | 34 | [{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config}] 35 | # indent_size = 2 36 | 37 | [{*.htm,*.html,*.sht,*.shtm,*.shtml}] 38 | # indent_style = tab 39 | 40 | [{*.http,*.rest}] 41 | # indent_size = 0 42 | 43 | [{*.yaml,*.yml}] 44 | # indent_size = 2 45 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /src/components/EmojiRenderer.tsx: -------------------------------------------------------------------------------- 1 | import { useAppStore } from "@hooks/useAppStore"; 2 | import { ParsedEmoji } from "@/utils/emojiParser"; 3 | 4 | interface Props { 5 | emoji: ParsedEmoji; 6 | size?: number; 7 | } 8 | 9 | function EmojiRenderer({ emoji, size = 22 }: Props) { 10 | const app = useAppStore(); 11 | 12 | if (emoji.type === "custom" && emoji.id) { 13 | const customEmoji = app.emojis.get(emoji.id); 14 | if (customEmoji) { 15 | return ( 16 | {emoji.name} 28 | ); 29 | } 30 | } 31 | 32 | return :{emoji.name}:; 33 | } 34 | 35 | export default EmojiRenderer; 36 | -------------------------------------------------------------------------------- /src/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const LinkElement = styled.a<{ color?: string }>` 4 | // remove the default underline 5 | text-decoration: none; 6 | // set the color to the primary color 7 | color: ${(props) => props.color || "var(--primary-light)"}; 8 | cursor: pointer; 9 | 10 | // remove the color when already visited because ew 11 | &:visited { 12 | color: ${(props) => props.color || "var(--primary-light)"}; 13 | } 14 | 15 | // when hovered, add underline 16 | &:hover { 17 | text-decoration: underline; 18 | } 19 | `; 20 | 21 | export default function Link(props: React.AnchorHTMLAttributes) { 22 | return ( 23 | { 25 | // allow the default context menu to open 26 | e.stopPropagation(); 27 | }} 28 | {...props} 29 | /> 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/controllers/modals/ModalRenderer.tsx: -------------------------------------------------------------------------------- 1 | // adapted from https://github.com/revoltchat/revite/blob/master/src/controllers/modals/ModalRenderer.tsx 2 | // Removed usage of `Prompt` 3 | 4 | import { observer } from "mobx-react-lite"; 5 | import { useEffect } from "react"; 6 | import { modalController } from "./ModalController"; 7 | 8 | export default observer(() => { 9 | useEffect(() => { 10 | function keyDown(event: KeyboardEvent) { 11 | if (event.key === "Escape") { 12 | modalController.pop("close"); 13 | } else if (event.key === "Enter") { 14 | if (event.target instanceof HTMLSelectElement) return; 15 | modalController.pop("confirm"); 16 | } 17 | } 18 | 19 | document.addEventListener("keydown", keyDown); 20 | return () => document.removeEventListener("keydown", keyDown); 21 | }, []); 22 | 23 | return <>{modalController.rendered}; 24 | }); 25 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/modals/TextAttachmentViewerModal.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { Modal } from "./ModalComponents"; 3 | 4 | const Container = styled.div` 5 | display: flex; 6 | overflow: hidden; 7 | flex-direction: column; 8 | max-width: 90vw; 9 | max-height: 75vh; 10 | overflow-x: scroll; 11 | overflow-y: scroll; 12 | user-select: text; 13 | padding: 16px; 14 | white-space: pre-wrap; 15 | `; 16 | 17 | interface Props { 18 | text: string; 19 | } 20 | 21 | export function TextAttachmentViewerModal(props: Props) { 22 | return ( 23 | 35 | 36 | {props.text} 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /scripts/tauri-version.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import process from "process"; 4 | 5 | // if (!process.env.CI) { 6 | // console.log("Not running in CI, skipping. Please do not run this script manually!"); 7 | // process.exit(0); 8 | // } 9 | 10 | const GITHUB_RUN_ID = process.env.GITHUB_RUN_ID || "0"; 11 | const GITHUB_RUN_ATTEMPT = process.env.GITHUB_RUN_ATTEMPT || "0"; 12 | const GITHUB_REF_NAME = process.env.GITHUB_REF_NAME; 13 | 14 | const pkgJsonPath = path.resolve("./package.json"); 15 | const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")); 16 | const pkgVersion = pkgJson.version; 17 | 18 | // const tauriJsonPath = path.resolve("./src-tauri/tauri.conf.json"); 19 | const tauriJsonPath = path.resolve("./src-tauri/version.json"); 20 | const tauriJson = { 21 | version: `${pkgVersion}+${GITHUB_RUN_ID}${GITHUB_RUN_ATTEMPT}`, 22 | }; 23 | fs.writeFileSync(tauriJsonPath, JSON.stringify(tauriJson, null, 4)); 24 | -------------------------------------------------------------------------------- /src/stores/EmojiStore.ts: -------------------------------------------------------------------------------- 1 | import type { APIEmoji } from "@spacebarchat/spacebar-api-types/v9"; 2 | import Emoji from "./objects/Emoji"; 3 | import { action, computed, makeAutoObservable, observable, ObservableMap } from "mobx"; 4 | import AppStore from "./AppStore"; 5 | 6 | export default class EmojiStore { 7 | private readonly app: AppStore; 8 | @observable private readonly emojis: ObservableMap; 9 | 10 | constructor(app: AppStore) { 11 | this.app = app; 12 | this.emojis = observable.map(); 13 | makeAutoObservable(this); 14 | } 15 | 16 | @action 17 | add(emoji: APIEmoji) { 18 | if (!emoji.id) return; 19 | this.emojis.set(emoji.id, new Emoji(emoji)); 20 | } 21 | 22 | @action 23 | addAll(emojis: APIEmoji[]) { 24 | emojis.forEach((emoji) => this.add(emoji)); 25 | } 26 | 27 | get(id: string) { 28 | return this.emojis.get(id); 29 | } 30 | 31 | @computed 32 | get all() { 33 | return Array.from(this.emojis.values()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/AuthenticationGuard.tsx: -------------------------------------------------------------------------------- 1 | import { useAppStore } from "@hooks/useAppStore"; 2 | import { LoadingSuspense } from "@pages/LoadingPage"; 3 | import { Navigate } from "react-router-dom"; 4 | 5 | interface Props { 6 | component: React.FC; 7 | requireUnauthenticated?: boolean; 8 | } 9 | 10 | export default function AuthenticationGuard({ component, requireUnauthenticated }: Props) { 11 | const app = useAppStore(); 12 | 13 | // if we need the user to be logged in, and there isn't a token, go to login page 14 | if (!requireUnauthenticated && !app.token) { 15 | return ; 16 | } 17 | 18 | // if we need the user to be logged out to access the page, but there is a token, go to the app page 19 | if (requireUnauthenticated && app.token) { 20 | return ; 21 | } 22 | 23 | const Component = component; 24 | return ( 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/hooks/useWindowResize.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/ruvkr/react-components-by-ruvkr/blob/master/src/hooks/useWindowResize.ts 2 | 3 | import { useCallback, useEffect, useRef } from "react"; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-empty-function 6 | export const useWindowResize = (callback: () => void = () => {}, interval = 100) => { 7 | const resizeTimeout = useRef(null); 8 | 9 | const resizeHandler = useCallback(() => { 10 | if (resizeTimeout.current != null) clearTimeout(resizeTimeout.current); 11 | resizeTimeout.current = setTimeout(() => { 12 | resizeTimeout.current = null; 13 | callback(); 14 | }, interval); 15 | }, [interval, callback]); 16 | 17 | useEffect(() => { 18 | window.addEventListener("resize", resizeHandler); 19 | return () => { 20 | if (resizeTimeout.current != null) clearTimeout(resizeTimeout.current); 21 | window.removeEventListener("resize", resizeHandler); 22 | }; 23 | }, [resizeHandler]); 24 | }; 25 | -------------------------------------------------------------------------------- /public/splashscreen.css: -------------------------------------------------------------------------------- 1 | /* roboto-latin-400-normal */ 2 | @font-face { 3 | font-family: "Roboto"; 4 | font-style: normal; 5 | font-display: swap; 6 | font-weight: 400; 7 | src: url(https://cdn.jsdelivr.net/fontsource/fonts/roboto@latest/latin-400-normal.woff2) format("woff2"), 8 | url(https://cdn.jsdelivr.net/fontsource/fonts/roboto@latest/latin-400-normal.woff) format("woff"); 9 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, 10 | U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 11 | } 12 | 13 | * { 14 | padding: 0; 15 | margin: 0; 16 | } 17 | 18 | body { 19 | overflow: hidden; 20 | background-color: #121212; 21 | color: #fff; 22 | font-family: "Roboto", sans-serif; 23 | } 24 | 25 | svg { 26 | max-width: 80vw; 27 | } 28 | 29 | .container { 30 | display: flex; 31 | flex-direction: column; 32 | justify-content: center; 33 | align-items: center; 34 | height: 100vh; 35 | } 36 | -------------------------------------------------------------------------------- /src/components/messaging/MessageGroup.tsx: -------------------------------------------------------------------------------- 1 | import { MessageType } from "@spacebarchat/spacebar-api-types/v9"; 2 | import { MessageGroup as MessageGroupType } from "@stores/MessageStore"; 3 | import { observer } from "mobx-react-lite"; 4 | import Message from "./Message"; 5 | import SystemMessage from "./SystemMessage"; 6 | 7 | interface Props { 8 | group: MessageGroupType; 9 | } 10 | 11 | /** 12 | * Component that handles rendering a group of messages from the same author 13 | */ 14 | function MessageGroup({ group }: Props) { 15 | const { messages } = group; 16 | 17 | return ( 18 | <> 19 | {messages.map((message, index) => { 20 | if (message.type === MessageType.Default || message.type === MessageType.Reply) { 21 | return ; 22 | } else return ; 23 | })} 24 | 25 | ); 26 | } 27 | 28 | export default observer(MessageGroup); 29 | -------------------------------------------------------------------------------- /src/stores/objects/ReadState.ts: -------------------------------------------------------------------------------- 1 | import { type APIReadState } from "@spacebarchat/spacebar-api-types/v9"; 2 | import { AppStore } from "@stores"; 3 | import { Logger } from "@utils"; 4 | import { action, makeAutoObservable, observable } from "mobx"; 5 | 6 | export default class ReadState { 7 | private readonly logger: Logger; 8 | private readonly app: AppStore; 9 | 10 | id: string; 11 | @observable lastMessageId: string; 12 | @observable lastPinTimestamp: string | null; 13 | @observable mentionCount: number | null; 14 | 15 | constructor(app: AppStore, data: APIReadState) { 16 | this.logger = new Logger("ReadState"); 17 | this.app = app; 18 | 19 | this.id = data.id; // channel id 20 | this.lastMessageId = data.last_message_id; 21 | this.lastPinTimestamp = data.last_pin_timestamp; 22 | this.mentionCount = data.mention_count; 23 | 24 | makeAutoObservable(this); 25 | } 26 | 27 | @action 28 | update(role: APIReadState) { 29 | Object.assign(this, role); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/mui/setRef.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/mui/material-ui/blob/master/packages/mui-utils/src/setRef/setRef.ts 2 | 3 | import * as React from "react"; 4 | 5 | /** 6 | * TODO v5: consider making it private 7 | * 8 | * passes {value} to {ref} 9 | * 10 | * WARNING: Be sure to only call this inside a callback that is passed as a ref. 11 | * Otherwise, make sure to cleanup the previous {ref} if it changes. See 12 | * https://github.com/mui/material-ui/issues/13539 13 | * 14 | * Useful if you want to expose the ref of an inner component to the public API 15 | * while still using it inside the component. 16 | * @param ref A ref callback or ref object. If anything falsy, this is a no-op. 17 | */ 18 | export default function setRef( 19 | ref: React.MutableRefObject | ((instance: T | null) => void) | null | undefined, 20 | value: T | null, 21 | ): void { 22 | if (typeof ref === "function") { 23 | ref(value); 24 | } else if (ref) { 25 | ref.current = value; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/emojiParser.ts: -------------------------------------------------------------------------------- 1 | export interface ParsedEmoji { 2 | type: "unicode" | "custom"; 3 | id: string; 4 | name: string; 5 | animated?: boolean; 6 | unified?: string; 7 | } 8 | 9 | export function parseEmojiString(content: string): (string | ParsedEmoji)[] { 10 | // Discord-style emoji regex: <:name:id> or 11 | const emojiRegex = /<(a?):(\w+):(\d+)>/g; 12 | const parts: (string | ParsedEmoji)[] = []; 13 | let lastIndex = 0; 14 | let match; 15 | 16 | while ((match = emojiRegex.exec(content)) !== null) { 17 | // Add text before emoji 18 | if (match.index > lastIndex) { 19 | parts.push(content.slice(lastIndex, match.index)); 20 | } 21 | 22 | parts.push({ 23 | type: "custom", 24 | animated: match[1] === "a", 25 | name: match[2], 26 | id: match[3], 27 | }); 28 | 29 | lastIndex = match.index + match[0].length; 30 | } 31 | 32 | // Add remaining text 33 | if (lastIndex < content.length) { 34 | parts.push(content.slice(lastIndex)); 35 | } 36 | 37 | return parts; 38 | } 39 | -------------------------------------------------------------------------------- /src/stores/objects/Emoji.ts: -------------------------------------------------------------------------------- 1 | import { REST } from "@/utils"; 2 | import { CDNRoutes, ImageFormat, type APIEmoji, type APIUser } from "@spacebarchat/spacebar-api-types/v9"; 3 | import { makeAutoObservable, observable } from "mobx"; 4 | 5 | export default class Emoji { 6 | @observable id: string; 7 | @observable name: string; 8 | @observable roles?: string[]; 9 | @observable user?: APIUser; 10 | @observable require_colons?: boolean; 11 | @observable managed?: boolean; 12 | @observable animated?: boolean; 13 | @observable available?: boolean; 14 | 15 | constructor(data: APIEmoji) { 16 | this.id = data.id!; 17 | this.name = data.name!; 18 | this.roles = data.roles; 19 | this.user = data.user; 20 | this.require_colons = data.require_colons; 21 | this.managed = data.managed; 22 | this.animated = data.animated; 23 | this.available = data.available; 24 | 25 | makeAutoObservable(this); 26 | } 27 | 28 | get imageUrl() { 29 | return REST.makeCDNUrl(CDNRoutes.emoji(this.id, ImageFormat.PNG)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /notice/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Spacebar 7 | 8 | 28 | 29 | 30 |

31 | The React Spacebar client is no longer actively maintained. Because of growing issues, the client has been 32 | taken offline.
33 | Please check out Fermi as an alternative client which is actively 34 | maintained and offers way more features than this client ever did. 35 |

36 |

- Puyodead1

37 | 38 | 39 | -------------------------------------------------------------------------------- /src/contexts/ContextMenuContext.ts: -------------------------------------------------------------------------------- 1 | import { useFloating } from "@floating-ui/react"; 2 | import { Channel, Guild, GuildMember, MessageLike, User } from "@structures"; 3 | import React from "react"; 4 | 5 | export type ContextMenuProps = 6 | | { 7 | type: "user"; 8 | user: User; 9 | member?: GuildMember; 10 | } 11 | | { 12 | type: "message"; 13 | message: MessageLike; 14 | } 15 | | { 16 | type: "channel"; 17 | channel: Channel; 18 | } 19 | | { 20 | type: "channelMention"; 21 | channel: Channel; 22 | } 23 | | { 24 | type: "guild"; 25 | guild: Guild; 26 | }; 27 | 28 | export type ContextMenuContextType = { 29 | setReferenceElement: ReturnType["refs"]["setReference"]; 30 | onContextMenu: (e: React.MouseEvent, props: ContextMenuProps) => void; 31 | close: () => void; 32 | open: (props: ContextMenuProps) => void; 33 | }; 34 | 35 | // @ts-expect-error not specifying a default value here 36 | export const ContextMenuContext = React.createContext(); 37 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | width: 100%; 6 | overflow: hidden; 7 | display: flex; 8 | } 9 | 10 | body { 11 | margin: 0; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | /* overflow: hidden; */ 15 | } 16 | 17 | *, 18 | *:after, 19 | *:before { 20 | box-sizing: border-box; 21 | } 22 | 23 | html *:not(code) { 24 | font-family: var(--font-family); 25 | } 26 | 27 | code { 28 | font-family: "Source Code Pro", monospace; 29 | } 30 | 31 | /* Scroll bar stylings */ 32 | ::-webkit-scrollbar { 33 | width: 16px; 34 | height: 16px; 35 | } 36 | 37 | /* Track */ 38 | ::-webkit-scrollbar-track { 39 | background: var(--scrollbar-track); 40 | border: 4px solid transparent; 41 | border-radius: 8px; 42 | background-clip: padding-box; 43 | } 44 | 45 | /* Handle */ 46 | ::-webkit-scrollbar-thumb { 47 | background-color: var(--scrollbar-thumb); 48 | min-height: 40px; 49 | border: 4px solid transparent; 50 | background-clip: padding-box; 51 | border-radius: 8px; 52 | } 53 | -------------------------------------------------------------------------------- /src/components/modals/AddServerModal.tsx: -------------------------------------------------------------------------------- 1 | import { ModalProps, modalController } from "@/controllers/modals"; 2 | import Button from "@components/Button"; 3 | import styled from "styled-components"; 4 | import { Modal } from "./ModalComponents"; 5 | 6 | const ActionWrapper = styled.div` 7 | display: flex; 8 | flex-direction: column; 9 | gap: 8px; 10 | `; 11 | export function AddServerModal({ ...props }: ModalProps<"add_server">) { 12 | return ( 13 | 14 | 15 | 26 | 27 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/stores/ThemeStore.ts: -------------------------------------------------------------------------------- 1 | import { ThemePresets, type Theme } from "@contexts/Theme"; 2 | import { PresenceUpdateStatus } from "@spacebarchat/spacebar-api-types/v9"; 3 | import { computed, makeAutoObservable } from "mobx"; 4 | 5 | export default class ThemeStore { 6 | constructor() { 7 | makeAutoObservable(this); 8 | } 9 | 10 | @computed 11 | getVariables(): Theme { 12 | return { 13 | ...ThemePresets["dark"], 14 | light: false, 15 | }; 16 | } 17 | 18 | @computed 19 | computeVariables() { 20 | const variables = this.getVariables(); 21 | 22 | return variables as unknown as Theme; 23 | } 24 | 25 | @computed 26 | getStatusColor(status?: PresenceUpdateStatus) { 27 | switch (status) { 28 | case PresenceUpdateStatus.Online: 29 | return ThemePresets["dark"].successLight; 30 | case PresenceUpdateStatus.Idle: 31 | return ThemePresets["dark"].statusIdle; 32 | case PresenceUpdateStatus.DoNotDisturb: 33 | return ThemePresets["dark"].dangerLight; 34 | case PresenceUpdateStatus.Offline: 35 | default: 36 | return ThemePresets["dark"].statusOffline; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/custom.d.ts: -------------------------------------------------------------------------------- 1 | // declare module "*.svg" { 2 | // import React = require("react"); 3 | // export const ReactComponent: React.FC>; 4 | // const src: string; 5 | // export default src; 6 | // } 7 | 8 | interface GlobalVersionInfo { 9 | tauri: string; 10 | app: string; 11 | platform: { 12 | name: string; 13 | arch: string; 14 | version: string; 15 | locale: string | null; 16 | }; 17 | } 18 | 19 | interface Window { 20 | windowToggleFps: () => void; 21 | globals: { 22 | tauriVersion: string; 23 | appVersion: string; 24 | platform: { 25 | name: string; 26 | arch: string; 27 | version: string; 28 | locale: string | null; 29 | }; 30 | }; 31 | updater: { 32 | setUpdateAvailable: (value: boolean) => void; 33 | setUpdateDownloading: (value: boolean) => void; 34 | setUpdateDownloaded: (value: boolean) => void; 35 | setCheckingForUpdates: (value: boolean) => void; 36 | checkForUpdates: () => Promise; 37 | downloadUpdate: () => Promise; 38 | quitAndInstall: () => Promise; 39 | clearUpdateCache: () => Promise; 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/mui/useForkRef.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/mui/material-ui/blob/master/packages/mui-utils/src/useForkRef/useForkRef.ts 2 | 3 | import React from "react"; 4 | import setRef from "./setRef"; 5 | 6 | export default function useForkRef( 7 | ...refs: Array | undefined> 8 | ): React.RefCallback | null { 9 | /** 10 | * This will create a new function if the refs passed to this hook change and are all defined. 11 | * This means react will call the old forkRef with `null` and the new forkRef 12 | * with the ref. Cleanup naturally emerges from this behavior. 13 | */ 14 | return React.useMemo(() => { 15 | if (refs.every((ref) => ref == null)) { 16 | return null; 17 | } 18 | 19 | return (instance) => { 20 | refs.forEach((ref) => { 21 | setRef(ref, instance); 22 | }); 23 | }; 24 | // TODO: uncomment once we enable eslint-plugin-react-compiler // eslint-disable-next-line react-compiler/react-compiler -- intentionally ignoring that the dependency array must be an array literal 25 | // eslint-disable-next-line react-hooks/exhaustive-deps 26 | }, refs); 27 | } 28 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AccountStore } from "./AccountStore"; 2 | export { default as AppStore } from "./AppStore"; 3 | export { default as ChannelStore } from "./ChannelStore"; 4 | export { default as ExperimentsStore } from "./ExperimentsStore"; 5 | export { default as GatewayConnectionStore } from "./GatewayConnectionStore"; 6 | export { default as GuildMemberListStore } from "./GuildMemberListStore"; 7 | export { default as GuildMemberStore } from "./GuildMemberStore"; 8 | export { default as GuildStore } from "./GuildStore"; 9 | export { default as MessageQueue } from "./MessageQueue"; 10 | export * from "./MessageStore"; 11 | export { default as MessageStore } from "./MessageStore"; 12 | export { default as PresenceStore } from "./PresenceStore"; 13 | export { default as PrivateChannelStore } from "./PrivateChannelStore"; 14 | export { default as ReadStateStore } from "./ReadStateStore"; 15 | export { default as RoleStore } from "./RoleStore"; 16 | export { default as ThemeStore } from "./ThemeStore"; 17 | export { default as UpdaterStore } from "./UpdaterStore"; 18 | export { default as UserStore } from "./UserStore"; 19 | -------------------------------------------------------------------------------- /.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 | **Console Logs** 24 | If applicable, add console logs to help give more information about your problem. 25 | 26 | **System Information (please complete the following information):** 27 | - OS: [e.g. Debian Linux, Arch Linux etc.] 28 | - Version (If not applicable skip): [e.g Ubuntu 22.04 LTS/Windows Server 2022] 29 | - Node Version: [e.g Node v18.7.0] 30 | 31 | **Env and Software info** 32 | - Release: [e.g. 0.1.0] 33 | - Branch (if release is not applicable): [e.g master] 34 | - Commit Hash (if release is not applicable): [e.g 401eda069a3ced17f1c43294d19765663cb8dcb7] 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /src/assets/images/logo/Spacebar_Icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/floating/FloatingContent.tsx: -------------------------------------------------------------------------------- 1 | import { FloatingFocusManager, FloatingPortal, useMergeRefs } from "@floating-ui/react"; 2 | import useFloatingContext from "@hooks/useFloatingContext"; 3 | import { motion } from "framer-motion"; 4 | import React from "react"; 5 | 6 | export default React.forwardRef>(function PopoverContent( 7 | { style, ...props }, 8 | propRef, 9 | ) { 10 | const { context: floatingContext, ...context } = useFloatingContext(); 11 | const ref = useMergeRefs([context.refs.setFloating, propRef]); 12 | 13 | if (!floatingContext.open) return null; 14 | 15 | return ( 16 | 17 | 18 | 24 |
29 | {props.children} 30 |
31 |
32 |
33 |
34 | ); 35 | }); 36 | -------------------------------------------------------------------------------- /src/components/SidebarPill.tsx: -------------------------------------------------------------------------------- 1 | import Container from "@components/Container"; 2 | import styled from "styled-components"; 3 | 4 | export type PillType = "none" | "unread" | "hover" | "active"; 5 | 6 | const Wrapper = styled(Container)` 7 | position: absolute; 8 | // top: 0; 9 | left: 0; 10 | width: 8px; 11 | height: 48px; 12 | display: flex; 13 | justify-content: flex-start; 14 | align-items: center; 15 | background-color: inherit; 16 | `; 17 | 18 | const Pill = styled.span<{ type: PillType }>` 19 | width: 8px; 20 | border-radius: 0 4px 4px 0; 21 | background-color: white; 22 | margin-left: -4px; 23 | transition: height 0.3s ease; 24 | 25 | ${(props) => { 26 | switch (props.type) { 27 | case "unread": 28 | return ` 29 | height: 8px; 30 | `; 31 | case "hover": 32 | return ` 33 | height: 20px; 34 | `; 35 | case "active": 36 | return ` 37 | height: 40px; 38 | `; 39 | default: 40 | return ` 41 | height: 0; 42 | `; 43 | } 44 | }} 45 | `; 46 | 47 | interface Props { 48 | type: PillType; 49 | } 50 | 51 | function SidebarPill({ type }: Props) { 52 | return ( 53 | 54 | 55 | 56 | ); 57 | } 58 | 59 | export default SidebarPill; 60 | -------------------------------------------------------------------------------- /src/components/floating/FloatingTrigger.tsx: -------------------------------------------------------------------------------- 1 | import { useMergeRefs } from "@floating-ui/react"; 2 | import useFloatingContext from "@hooks/useFloatingContext"; 3 | import React from "react"; 4 | 5 | interface PopoverTriggerProps { 6 | children: React.ReactNode; 7 | asChild?: boolean; 8 | } 9 | 10 | export default React.forwardRef & PopoverTriggerProps>( 11 | function FloatingTrigger({ children, asChild = false, ...props }, propRef) { 12 | const context = useFloatingContext(); 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | const childrenRef = (children as any).ref; 15 | const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]); 16 | 17 | // `asChild` allows the user to pass any element as the anchor 18 | if (asChild && React.isValidElement(children)) { 19 | return React.cloneElement( 20 | children, 21 | context.getReferenceProps({ 22 | ref, 23 | ...props, 24 | ...children.props, 25 | "data-state": context.open ? "open" : "closed", 26 | }), 27 | ); 28 | } 29 | 30 | return ( 31 | 32 | {children} 33 | 34 | ); 35 | }, 36 | ); 37 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "app" 3 | version = "0.0.0" 4 | description = "Spacebar Client" 5 | authors = ["Puyodead1"] 6 | license = "AGPL-3.0-only" 7 | repository = "https://github.com/spacebarchat/client" 8 | edition = "2021" 9 | 10 | [lib] 11 | name = "spacebar" 12 | crate-type = ["staticlib", "cdylib", "rlib"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [build-dependencies] 17 | tauri-build = { version = "2.0.4", features = [] } 18 | 19 | [dependencies] 20 | tauri = { version = "2.1.1", features = ["devtools", "tray-icon"] } 21 | tauri-plugin-updater = "2.8.1" 22 | tauri-plugin-process = "2.2.0" 23 | tauri-plugin-log = "2.5.1" 24 | tauri-plugin-os = "2.2.1" 25 | tauri-plugin-notification = "2.2.2" 26 | tauri-plugin-single-instance = "2.2.0" 27 | tauri-plugin-autostart = "2.2.0" 28 | reqwest = { version = "0.12.23", default-features = false, features = [ 29 | "json", 30 | "rustls-tls", 31 | ] } 32 | url = "2.5.4" 33 | chrono = "0.4" 34 | log = "0.4.22" 35 | serde = { version = "1.0", features = ["derive"] } 36 | serde_json = "1.0" 37 | 38 | [features] 39 | # this feature is used for production builds or when `devPath` points to the filesystem 40 | # DO NOT REMOVE!! 41 | custom-protocol = ["tauri/custom-protocol"] 42 | -------------------------------------------------------------------------------- /src/components/contextMenus/ChannelMentionContextMenu.tsx: -------------------------------------------------------------------------------- 1 | // loosely based on https://github.com/revoltchat/frontend/blob/master/components/app/menus/UserContextMenu.tsx 2 | 3 | import Channel from "@structures/Channel"; 4 | import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "./ContextMenu"; 5 | 6 | interface MenuProps { 7 | channel: Channel; 8 | } 9 | 10 | function ChannelMentionContextMenu({ channel }: MenuProps) { 11 | /** 12 | * Copy id to clipboard 13 | */ 14 | function copyId() { 15 | navigator.clipboard.writeText(channel.id); 16 | } 17 | 18 | /** 19 | * Copy link to clipboard 20 | */ 21 | function copyLink() { 22 | navigator.clipboard.writeText(`${window.location.origin}/channels/${channel.guildId}/${channel.id}`); 23 | } 24 | 25 | return ( 26 | 27 | 28 | Copy Link 29 | 30 | 31 | 42 | Copy Channel ID 43 | 44 | 45 | ); 46 | } 47 | 48 | export default ChannelMentionContextMenu; 49 | -------------------------------------------------------------------------------- /src/stores/objects/Role.ts: -------------------------------------------------------------------------------- 1 | import type { APIRole, APIRoleTags } from "@spacebarchat/spacebar-api-types/v9"; 2 | import { AppStore } from "@stores"; 3 | import { action, makeAutoObservable, observable } from "mobx"; 4 | 5 | export default class Role { 6 | private readonly app: AppStore; 7 | 8 | id: string; 9 | @observable name: string; 10 | @observable color: string; 11 | @observable hoist: boolean; 12 | @observable icon?: string | null | undefined; 13 | @observable unicode_emoji?: string | null | undefined; 14 | @observable position: number; 15 | @observable permissions: string; 16 | managed: boolean; 17 | @observable mentionable: boolean; 18 | @observable tags?: APIRoleTags | undefined; 19 | 20 | constructor(app: AppStore, data: APIRole) { 21 | this.app = app; 22 | 23 | this.id = data.id; 24 | this.name = data.name; 25 | this.color = "#" + data.color.toString(16).padStart(6, "0"); 26 | this.hoist = data.hoist; 27 | this.icon = data.icon; 28 | this.unicode_emoji = data.unicode_emoji; 29 | this.position = data.position; 30 | this.permissions = data.permissions; 31 | this.managed = data.managed; 32 | this.mentionable = data.mentionable; 33 | this.tags = data.tags; 34 | 35 | makeAutoObservable(this); 36 | } 37 | 38 | @action 39 | update(role: APIRole) { 40 | Object.assign(this, role); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/stores/objects/Presence.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | APIUser, 3 | GatewayActivity, 4 | GatewayPresenceClientStatus, 5 | GatewayPresenceUpdateDispatchData, 6 | PresenceUpdateStatus, 7 | Snowflake, 8 | } from "@spacebarchat/spacebar-api-types/v9"; 9 | import { AppStore } from "@stores"; 10 | import { action, makeAutoObservable, observable } from "mobx"; 11 | import User from "./User"; 12 | 13 | export default class Presence { 14 | private readonly app: AppStore; 15 | 16 | @observable public readonly user: User; 17 | @observable public readonly guildId?: Snowflake; 18 | @observable public readonly status: PresenceUpdateStatus | undefined; 19 | @observable public readonly activities: GatewayActivity[] | undefined; 20 | @observable public readonly clientStatus: GatewayPresenceClientStatus | undefined; 21 | 22 | constructor(app: AppStore, data: GatewayPresenceUpdateDispatchData) { 23 | this.app = app; 24 | 25 | this.user = this.app.users.get(data.user.id) ?? new User(data.user as APIUser); // TODO: is this right? 26 | this.guildId = data.guild_id; 27 | this.status = data.status; 28 | this.activities = data.activities; 29 | this.clientStatus = data.client_status; 30 | 31 | makeAutoObservable(this); 32 | } 33 | 34 | @action 35 | update(data: GatewayPresenceUpdateDispatchData) { 36 | Object.assign(this, data); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /flake.template.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Spacebar client, written in React."; 3 | 4 | #inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11"; 5 | inputs.flake-utils.url = "github:numtide/flake-utils"; 6 | #inputs.pnpm2nix.url = "github:TheArcaneBrony/pnpm2nix"; 7 | #inputs.pnpm2nix.url = "path:/home/root@Rory/git/spacebar/client/pnpm2nix"; 8 | #inputs.pnpm2nix.flake = false; 9 | inputs.nixpkgs.url = "github:lilyinstarlight/nixpkgs/unheck/nodejs"; 10 | 11 | outputs = { self, nixpkgs, flake-utils }: 12 | flake-utils.lib.eachDefaultSystem (system: 13 | let 14 | pkgs = import nixpkgs { 15 | inherit system; 16 | }; 17 | #_pnpm2nix = import pnpm2nix { pkgs = nixpkgs.legacyPackages.${system}; }; 18 | in rec { 19 | #packages.default = _pnpm2nix.mkPnpmPackage { 20 | packages.default = pkgs.buildNpmPackage { 21 | pname = "spacebar-client-react"; 22 | src = ./.; 23 | name = "spacebar-client-react"; 24 | #buildInputs = with pkgs; [ ]; 25 | npmDepsHash = "$NPM_HASH"; 26 | makeCacheWritable = true; 27 | installPhase = '' 28 | runHook preInstall 29 | cp -r dist $out/ 30 | runHook postInstall 31 | ''; 32 | }; 33 | devShell = pkgs.mkShell { 34 | buildInputs = with pkgs; [ 35 | nodejs 36 | nodePackages.pnpm 37 | nodePackages.typescript 38 | ]; 39 | }; 40 | } 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Spacebar client, written in React."; 3 | 4 | #inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11"; 5 | inputs.flake-utils.url = "github:numtide/flake-utils"; 6 | #inputs.pnpm2nix.url = "github:TheArcaneBrony/pnpm2nix"; 7 | #inputs.pnpm2nix.url = "path:/home/root@Rory/git/spacebar/client/pnpm2nix"; 8 | #inputs.pnpm2nix.flake = false; 9 | inputs.nixpkgs.url = "github:lilyinstarlight/nixpkgs/unheck/nodejs"; 10 | 11 | outputs = { self, nixpkgs, flake-utils }: 12 | flake-utils.lib.eachDefaultSystem (system: 13 | let 14 | pkgs = import nixpkgs { 15 | inherit system; 16 | }; 17 | #_pnpm2nix = import pnpm2nix { pkgs = nixpkgs.legacyPackages.${system}; }; 18 | in rec { 19 | #packages.default = _pnpm2nix.mkPnpmPackage { 20 | packages.default = pkgs.buildNpmPackage { 21 | pname = "spacebar-client-react"; 22 | src = ./.; 23 | name = "spacebar-client-react"; 24 | #buildInputs = with pkgs; [ ]; 25 | npmDepsHash = "sha256-BAsUdPWJk8/QVaRjOELusOf3TGoft4o90FJ11ef3xJE="; 26 | makeCacheWritable = true; 27 | installPhase = '' 28 | runHook preInstall 29 | cp -r dist $out/ 30 | runHook postInstall 31 | ''; 32 | }; 33 | devShell = pkgs.mkShell { 34 | buildInputs = with pkgs; [ 35 | nodejs 36 | nodePackages.pnpm 37 | nodePackages.typescript 38 | ]; 39 | }; 40 | } 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "baseUrl": "./src", 11 | 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | 20 | /* Linting */ 21 | "strict": true, 22 | // "noUnusedLocals": true, 23 | // "noUnusedParameters": true, 24 | "noFallthroughCasesInSwitch": true, 25 | 26 | /* Path aliases */ 27 | "paths": { 28 | "@/*": ["./*"], 29 | "@utils": ["utils/index.ts"], 30 | "@utils/*": ["utils/*"], 31 | "@components": ["components/index.ts"], 32 | "@components/*": ["components/*"], 33 | "@assets/*": ["assets/*"], 34 | "@modals/*": ["modals/*"], 35 | "@pages/*": ["pages/*"], 36 | "@stores": ["stores/index.ts"], 37 | "@stores/*": ["stores/*"], 38 | "@hooks/*": ["hooks/*"], 39 | "@contexts/*": ["contexts/*"], 40 | "@structures": ["stores/objects/index.ts"], 41 | "@structures/*": ["stores/objects/*"] 42 | } 43 | }, 44 | "include": ["src", "src/custom.d.ts"], 45 | "references": [{ "path": "./tsconfig.node.json" }] 46 | } 47 | -------------------------------------------------------------------------------- /src/stores/PrivateChannelStore.ts: -------------------------------------------------------------------------------- 1 | import type { APIChannel } from "@spacebarchat/spacebar-api-types/v9"; 2 | import { Channel } from "@structures"; 3 | import { action, computed, makeAutoObservable, observable, ObservableMap } from "mobx"; 4 | import AppStore from "./AppStore"; 5 | 6 | export default class PrivateChannelStore { 7 | private readonly app: AppStore; 8 | @observable readonly channels: ObservableMap; 9 | 10 | constructor(app: AppStore) { 11 | this.app = app; 12 | this.channels = observable.map(); 13 | 14 | makeAutoObservable(this); 15 | } 16 | 17 | @action 18 | add(channel: APIChannel) { 19 | this.channels.set(channel.id, new Channel(this.app, channel)); 20 | } 21 | 22 | @action 23 | update(channel: APIChannel) { 24 | const existing = this.channels.get(channel.id); 25 | if (existing) { 26 | existing.update(channel); 27 | } else { 28 | this.add(channel); 29 | } 30 | } 31 | 32 | @action 33 | addAll(channels: APIChannel[]) { 34 | channels.forEach((channel) => this.add(channel)); 35 | } 36 | 37 | get(id: string) { 38 | return this.channels.get(id); 39 | } 40 | 41 | @computed 42 | get all() { 43 | return Array.from(this.channels.values()); 44 | } 45 | 46 | @action 47 | remove(id: string) { 48 | this.channels.delete(id); 49 | } 50 | 51 | @computed 52 | get count() { 53 | return this.channels.size; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/stores/GuildStore.ts: -------------------------------------------------------------------------------- 1 | import type { GatewayGuild } from "@spacebarchat/spacebar-api-types/v9"; 2 | import { Guild } from "@structures"; 3 | import { Logger } from "@utils"; 4 | import { action, computed, makeAutoObservable, observable, ObservableMap } from "mobx"; 5 | import AppStore from "./AppStore"; 6 | 7 | export default class GuildStore { 8 | private readonly logger: Logger = new Logger("GuildStore"); 9 | private readonly app: AppStore; 10 | @observable initialGuildsLoaded = false; 11 | @observable readonly guilds: ObservableMap; 12 | 13 | constructor(app: AppStore) { 14 | this.app = app; 15 | this.guilds = observable.map(); 16 | 17 | makeAutoObservable(this); 18 | } 19 | 20 | @action 21 | setInitialGuildsLoaded() { 22 | this.initialGuildsLoaded = true; 23 | this.logger.debug("Initial guilds loaded"); 24 | } 25 | 26 | @action 27 | add(guild: GatewayGuild) { 28 | this.guilds.set(guild.id, new Guild(this.app, guild)); 29 | } 30 | 31 | @action 32 | addAll(guilds: GatewayGuild[]) { 33 | guilds.forEach((guild) => this.add(guild)); 34 | } 35 | 36 | get(id: string) { 37 | return this.guilds.get(id); 38 | } 39 | 40 | @computed 41 | get all() { 42 | return Array.from(this.guilds.values()); 43 | } 44 | 45 | @action 46 | remove(id: string) { 47 | this.guilds.delete(id); 48 | } 49 | 50 | @computed 51 | get count() { 52 | return this.guilds.size; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/stores/RoleStore.ts: -------------------------------------------------------------------------------- 1 | import type { Snowflake } from "@spacebarchat/spacebar-api-types/globals"; 2 | import type { APIRole } from "@spacebarchat/spacebar-api-types/v9"; 3 | import { Role } from "@structures"; 4 | import { action, computed, makeAutoObservable, observable, ObservableMap } from "mobx"; 5 | import AppStore from "./AppStore"; 6 | 7 | export default class RoleStore { 8 | private readonly app: AppStore; 9 | @observable private readonly roles: ObservableMap; 10 | 11 | constructor(app: AppStore) { 12 | this.app = app; 13 | this.roles = observable.map(); 14 | 15 | makeAutoObservable(this); 16 | } 17 | 18 | @action 19 | add(role: APIRole) { 20 | this.roles.set(role.id, new Role(this.app, role)); 21 | } 22 | 23 | @action 24 | addAll(roles: APIRole[]) { 25 | roles.forEach((role) => this.add(role)); 26 | } 27 | 28 | @computed 29 | get all() { 30 | return Array.from(this.roles.values()); 31 | } 32 | 33 | @action 34 | remove(id: Snowflake) { 35 | this.roles.delete(id); 36 | } 37 | 38 | @action 39 | update(role: APIRole) { 40 | this.roles.get(role.id)?.update(role); 41 | } 42 | 43 | get(id: Snowflake) { 44 | return this.roles.get(id); 45 | } 46 | 47 | has(id: Snowflake) { 48 | return this.roles.has(id); 49 | } 50 | 51 | asList() { 52 | return Array.from(this.roles.values()); 53 | } 54 | 55 | get size() { 56 | return this.roles.size; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src-tauri/gen/android/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 31 | 32 | -------------------------------------------------------------------------------- /src/components/modals/DeleteMessageModal.tsx: -------------------------------------------------------------------------------- 1 | import { ModalProps, modalController } from "@/controllers/modals"; 2 | import MarkdownRenderer from "@components/markdown/MarkdownRenderer"; 3 | import styled from "styled-components"; 4 | import { Modal } from "./ModalComponents"; 5 | 6 | const PreviewContainer = styled.div` 7 | background-color: var(--background-secondary); 8 | margin-top: 16px; 9 | box-shadow: 0 0 0 1px var(--background-tertiary), 0 2px 10px 0 var(--background-tertiary); 10 | border-radius: 4px; 11 | overflow: hidden; 12 | padding: 5px 6px; 13 | `; 14 | 15 | export function DeleteMessageModal({ target, ...props }: ModalProps<"delete_message">) { 16 | return ( 17 | { 24 | modalController.pop("close"); 25 | }, 26 | children: Cancel, 27 | palette: "link", 28 | size: "small", 29 | confirmation: true, 30 | }, 31 | { 32 | onClick: async () => { 33 | await target.delete(); 34 | modalController.pop("close"); 35 | }, 36 | children: Delete, 37 | palette: "danger", 38 | size: "small", 39 | }, 40 | ]} 41 | > 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/contexts/ContextMenuContextProvider.tsx: -------------------------------------------------------------------------------- 1 | import { FloatingPortal } from "@floating-ui/react"; 2 | import useContextMenu, { ContextMenuComponents } from "@hooks/useContextMenu"; 3 | import React from "react"; 4 | import { ContextMenuContext, ContextMenuProps } from "./ContextMenuContext"; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | export const ContextMenuContextProvider: React.FC = ({ children }) => { 8 | const contextMenu = useContextMenu(); 9 | 10 | const open = (props: ContextMenuProps) => { 11 | contextMenu.open(props); 12 | }; 13 | 14 | const Component = contextMenu.props 15 | ? ContextMenuComponents[contextMenu.props.type] 16 | : () => { 17 | return null; 18 | }; 19 | 20 | return ( 21 | 29 | {children} 30 | 31 | {contextMenu.isOpen && ( 32 |
[contextMenu.close()]} 38 | > 39 | 40 |
41 | )} 42 |
43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/stores/objects/MessageBase.ts: -------------------------------------------------------------------------------- 1 | // base class for messages and queued messages 2 | 3 | import { Snowflake } from "@spacebarchat/spacebar-api-types/globals"; 4 | import { MessageType } from "@spacebarchat/spacebar-api-types/v9"; 5 | import { AppStore } from "@stores"; 6 | import { USER_JOIN_MESSAGES } from "@utils"; 7 | import { observable } from "mobx"; 8 | import { MessageLikeData } from "./Message"; 9 | import User from "./User"; 10 | 11 | export default class MessageBase { 12 | /** 13 | * ID of the message 14 | */ 15 | id: Snowflake; 16 | /** 17 | * Contents of the message 18 | */ 19 | @observable content: string; 20 | /** 21 | * When this message was sent 22 | */ 23 | timestamp: Date; 24 | /** 25 | * Type of message 26 | */ 27 | type: MessageType; 28 | author: User; 29 | 30 | constructor(public readonly app: AppStore, data: MessageLikeData) { 31 | this.id = data.id; 32 | this.content = data.content; 33 | this.timestamp = new Date(data.timestamp); 34 | this.type = data.type; 35 | 36 | if (this.app.users.has(data.author.id)) { 37 | this.author = this.app.users.get(data.author.id) as User; 38 | } else { 39 | const user = new User(data.author); 40 | this.app.users.users.set(user.id, user); 41 | this.author = user; 42 | } 43 | } 44 | 45 | getJoinMessage() { 46 | if (this.type !== MessageType.UserJoin) throw new Error("Message is not a user join message"); 47 | return USER_JOIN_MESSAGES[this.timestamp.getTime() % 13]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/assets/images/logo/icon-rounded.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 26 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/components/markdown/components/Timestamp.tsx: -------------------------------------------------------------------------------- 1 | import { Floating, FloatingTrigger } from "@components/floating"; 2 | import dayjs from "dayjs"; 3 | import { memo } from "react"; 4 | import styled from "styled-components"; 5 | 6 | const Container = styled.div` 7 | background-color: hsl(var(--background-tertiary-hsl) / 0.3); 8 | padding: 2px; 9 | border-radius: 4px; 10 | width: fit-content; 11 | `; 12 | 13 | interface Props { 14 | timestamp: string; 15 | style?: string; 16 | } 17 | 18 | function Timestamp({ timestamp, style }: Props) { 19 | const date = dayjs.unix(Number(timestamp)); 20 | 21 | let value = ""; 22 | switch (style) { 23 | case "t": 24 | value = date.format("hh:mm"); 25 | break; 26 | case "T": 27 | value = date.format("hh:mm:ss"); 28 | break; 29 | case "R": 30 | value = date.fromNow(); 31 | break; 32 | case "D": 33 | value = date.format("DD MMMM YYYY"); 34 | break; 35 | case "F": 36 | value = date.format("dddd, DD MMMM YYYY hh:mm"); 37 | break; 38 | case "f": 39 | default: 40 | value = date.format("DD MMMM YYYY hh:mm"); 41 | break; 42 | } 43 | 44 | return ( 45 | 46 | {date.format("dddd, MMMM MM, h:mm A")}, 51 | }} 52 | > 53 | 54 | {value} 55 | 56 | 57 | 58 | ); 59 | } 60 | 61 | export default memo(Timestamp); 62 | -------------------------------------------------------------------------------- /src-tauri/src/tray.rs: -------------------------------------------------------------------------------- 1 | use tauri::{ 2 | menu::{Menu, MenuItem}, 3 | tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, 4 | Manager, Runtime, 5 | }; 6 | 7 | pub fn create_tray(app: &tauri::AppHandle) -> tauri::Result<()> { 8 | let branding = MenuItem::with_id(app, "name", "Spacebar", false, None::)?; 9 | let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::)?; 10 | let menu1 = Menu::with_items(app, &[&branding, &quit_i])?; 11 | 12 | let _ = TrayIconBuilder::with_id("main") 13 | .tooltip("Spacebar") 14 | .icon(app.default_window_icon().unwrap().clone()) 15 | .menu(&menu1) 16 | .menu_on_left_click(false) 17 | .on_menu_event(move |app, event| match event.id.as_ref() { 18 | "quit" => { 19 | app.exit(0); 20 | } 21 | 22 | _ => {} 23 | }) 24 | .on_tray_icon_event(|tray, event| { 25 | if let TrayIconEvent::Click { 26 | button: MouseButton::Left, 27 | button_state: MouseButtonState::Up, 28 | .. 29 | } = event 30 | { 31 | let app = tray.app_handle(); 32 | if let Some(window) = app.get_webview_window("main") { 33 | let _ = window.show(); 34 | let _ = window.set_focus(); 35 | } 36 | } 37 | }) 38 | .build(app); 39 | 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /src/components/ChannelSidebar.tsx: -------------------------------------------------------------------------------- 1 | import Container from "@components/Container"; 2 | import { useWindowSize } from "@uidotdev/usehooks"; 3 | import { isTouchscreenDevice } from "@utils"; 4 | import { observer } from "mobx-react-lite"; 5 | import { useEffect, useState } from "react"; 6 | import { isDesktop } from "react-device-detect"; 7 | import styled from "styled-components"; 8 | import ChannelHeader from "./ChannelHeader"; 9 | import ChannelList from "./ChannelList/ChannelList"; 10 | import UserPanel from "./UserPanel"; 11 | 12 | const Wrapper = styled(Container)` 13 | display: flex; 14 | flex-direction: column; 15 | background-color: var(--background-secondary); 16 | `; 17 | 18 | function ChannelSidebar() { 19 | const windowSize = useWindowSize(); 20 | //const isSmallScreen = useMediaQuery("only screen and (max-width: 810px)"); 21 | const [size, setSize] = useState(); 22 | 23 | useEffect(() => { 24 | if (!windowSize.width) return; 25 | const screenPercent = (windowSize.width * 80) / 100; 26 | setSize(screenPercent - 72); 27 | }, [windowSize]); 28 | 29 | return ( 30 | 41 | {/* TODO: replace with dm search if no guild */} 42 | 43 | 44 | {!isTouchscreenDevice && } 45 | 46 | ); 47 | } 48 | 49 | export default observer(ChannelSidebar); 50 | -------------------------------------------------------------------------------- /src/components/modals/ImageViewerModal.tsx: -------------------------------------------------------------------------------- 1 | import { APIAttachment, APIEmbedImage, APIEmbedThumbnail, APIEmbedVideo } from "@spacebarchat/spacebar-api-types/v9"; 2 | import styled from "styled-components"; 3 | import { Modal } from "./ModalComponents"; 4 | 5 | const Container = styled.div` 6 | display: flex; 7 | overflow: hidden; 8 | flex-direction: column; 9 | max-width: 90vw; 10 | max-height: 75vh; 11 | 12 | img { 13 | object-fit: contain; 14 | } 15 | `; 16 | 17 | interface Props { 18 | attachment: APIAttachment | APIEmbedImage | APIEmbedThumbnail | APIEmbedVideo; 19 | width?: number; 20 | height?: number; 21 | isVideo?: boolean; // should only be for gifs 22 | } 23 | 24 | export function ImageViewerModal(props: Props) { 25 | const width = props.width ?? props.attachment.width ?? 0; 26 | const height = props.height ?? props.attachment.height ?? 0; 27 | 28 | return ( 29 | 38 | 39 | {props.isVideo ? ( 40 | 50 | ) : ( 51 | 52 | )} 53 | 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src-tauri/gen/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | android.defaults.buildfeatures.buildconfig=true 25 | android.nonFinalResIds=false -------------------------------------------------------------------------------- /src/components/ListSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | import styled from "styled-components"; 3 | import Icon from "./Icon"; 4 | 5 | const Container = styled.div` 6 | padding: 24px 8px 0 16px; 7 | `; 8 | 9 | const Title = styled.span` 10 | font-size: 12px; 11 | font-weight: var(--font-weight-bold); 12 | color: var(--text-secondary); 13 | user-select: none; 14 | cursor: pointer; 15 | 16 | display: flex; 17 | align-items: center; 18 | `; 19 | 20 | const Wrapper = styled.div<{ open?: boolean }>` 21 | margin-top: 4px; 22 | display: flex; 23 | flex-direction: column; 24 | display: ${(props) => (props.open === false ? "flex" : "none")}; 25 | `; 26 | 27 | const Item = styled.span` 28 | font-size: 16px; 29 | font-weight: var(--font-weight-medium); 30 | color: var(--text-normal); 31 | user-select: none; 32 | margin-left: 8px; 33 | padding: 4px 0; 34 | `; 35 | 36 | interface Props { 37 | name: string; 38 | items: ReactNode[]; 39 | } 40 | 41 | function ListSection(props: Props) { 42 | const [open, setOpen] = React.useState(false); 43 | const toggle = () => setOpen((prev) => !prev); 44 | 45 | return ( 46 | 47 | 48 | <Icon icon={open ? "mdiChevronRight" : "mdiChevronDown"} size={"16px"} /> 49 | {props.name} 50 | 51 | 52 | {/* {props.items.map((item, i) => ( 53 | {item} 54 | ))} */} 55 | {...props.items} 56 | 57 | 58 | ); 59 | } 60 | 61 | export default ListSection; 62 | -------------------------------------------------------------------------------- /src/stores/MessageQueue.ts: -------------------------------------------------------------------------------- 1 | import type { APIMessage } from "@spacebarchat/spacebar-api-types/v9"; 2 | import { action, computed, makeAutoObservable, observable, type IObservableArray } from "mobx"; 3 | 4 | import { QueuedMessage, QueuedMessageStatus, type QueuedMessageData } from "@structures"; 5 | import Snowflake from "@utils/Snowflake"; 6 | import AppStore from "./AppStore"; 7 | 8 | export default class MessageQueue { 9 | @observable readonly messages: IObservableArray; 10 | 11 | constructor(private readonly app: AppStore) { 12 | this.messages = observable.array([]); 13 | 14 | makeAutoObservable(this); 15 | } 16 | 17 | @action 18 | add(data: QueuedMessageData) { 19 | const msg = new QueuedMessage(this.app, data); 20 | this.messages.push(msg); 21 | return msg; 22 | } 23 | 24 | @action 25 | remove(id: string) { 26 | const message = this.messages.find((x) => x.id === id)!; 27 | this.messages.remove(message); 28 | } 29 | 30 | @action 31 | send(id: string) { 32 | const message = this.messages.find((x) => x.id === id)!; 33 | message.status = QueuedMessageStatus.SENDING; 34 | } 35 | 36 | @computed 37 | get(channel: Snowflake) { 38 | return this.messages.filter((message) => message.channel_id === channel); 39 | } 40 | 41 | @action 42 | handleIncomingMessage(message: APIMessage) { 43 | if (!message.nonce) { 44 | return; 45 | } 46 | if (!this.get(message.channel_id).find((x) => x.id === message.nonce)) { 47 | return; 48 | } 49 | 50 | this.remove(message.nonce.toString()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/pages-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Github Pages 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | 15 | - name: Install Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 19 19 | 20 | - uses: pnpm/action-setup@v2 21 | name: Install pnpm 22 | id: pnpm-install 23 | with: 24 | version: 8 25 | run_install: false 26 | 27 | - name: Get pnpm store directory 28 | id: pnpm-cache 29 | shell: bash 30 | run: | 31 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 32 | 33 | - uses: actions/cache@v3 34 | name: Setup pnpm cache 35 | with: 36 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 37 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 38 | restore-keys: | 39 | ${{ runner.os }}-pnpm-store- 40 | 41 | - name: Install dependencies 42 | run: pnpm install 43 | 44 | - name: Build 45 | run: pnpm build 46 | 47 | - run: echo dev.app.spacebar.chat >> build/CNAME 48 | 49 | - name: Deploy 50 | run: | 51 | git config user.name github-actions 52 | git config user.email github-actions@github.com 53 | git --work-tree build add --all 54 | git commit -m "Automatic Deploy" 55 | git push origin HEAD:gh-pages --force 56 | shell: bash 57 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | // export * from "./AuthComponents" 2 | export { default as Avatar } from "./Avatar"; 3 | export { default as Button } from "./Button"; 4 | export { default as ChannelHeader } from "./ChannelHeader"; 5 | export { default as ChannelSidebar } from "./ChannelSidebar"; 6 | export { default as Container } from "./Container"; 7 | export * from "./Divider"; 8 | export { default as DOBInput } from "./DOBInput"; 9 | export { default as ErrorBoundary } from "./ErrorBoundary"; 10 | // export * from "./FormComponents"; 11 | export * from "./banners"; 12 | export * from "./contextMenus"; 13 | export * from "./floating"; 14 | export { default as GuildItem } from "./GuildItem"; 15 | export { default as HCaptcha } from "./HCaptcha"; 16 | export { default as Icon } from "./Icon"; 17 | export { default as IconButton } from "./IconButton"; 18 | export { default as InviteEmbed } from "./InviteEmbed"; 19 | export { default as Link } from "./Link"; 20 | export { default as ListSection } from "./ListSection"; 21 | export { default as Loader } from "./Loader"; 22 | export * from "./markdown"; 23 | export * from "./media"; 24 | export * from "./SectionHeader"; 25 | export { default as SectionTitle } from "./SectionTitle"; 26 | export { default as SidebarAction } from "./SidebarAction"; 27 | export { default as SidebarPill } from "./SidebarPill"; 28 | export { default as SwipeableLayout } from "./SwipeableLayout"; 29 | export { default as Text } from "./Text"; 30 | export { default as Tooltip } from "./Tooltip"; 31 | export { default as UserPanel } from "./UserPanel"; 32 | -------------------------------------------------------------------------------- /src/pages/LoadingPage.tsx: -------------------------------------------------------------------------------- 1 | import SpacebarLogoBlue from "@assets/images/logo/Logo-Blue.svg?react"; 2 | import Button from "@components/Button"; 3 | import Container from "@components/Container"; 4 | import { useAppStore } from "@hooks/useAppStore"; 5 | import { observer } from "mobx-react-lite"; 6 | import { Suspense } from "react"; 7 | import PulseLoader from "react-spinners/PulseLoader"; 8 | import styled from "styled-components"; 9 | 10 | const Wrapper = styled.div` 11 | justify-content: center; 12 | align-items: center; 13 | display: flex; 14 | flex-direction: column; 15 | flex: 1; 16 | `; 17 | 18 | const SpacebarLogo = styled(SpacebarLogoBlue)` 19 | width: 80vw; 20 | height: min-content; 21 | margin-bottom: 32px; 22 | 23 | @media (min-width: 768px) { 24 | width: 40vw; 25 | } 26 | `; 27 | 28 | function LoadingPage() { 29 | const app = useAppStore(); 30 | 31 | return ( 32 | 37 | 38 | 39 | 40 | {app.token && ( 41 |
47 | 50 |
51 | )} 52 |
53 |
54 | ); 55 | } 56 | 57 | export default observer(LoadingPage); 58 | 59 | export const LoadingSuspense = ({ children }: { children: React.ReactNode }) => ( 60 | }>{children} 61 | ); 62 | -------------------------------------------------------------------------------- /src/components/modals/InviteModal.tsx: -------------------------------------------------------------------------------- 1 | import { ModalProps } from "@/controllers/modals"; 2 | import REST from "@utils/REST"; 3 | import styled from "styled-components"; 4 | import { Modal, ModalHeader, ModalHeaderText, ModalSubHeaderText } from "./ModalComponents"; 5 | 6 | const ActionWrapper = styled.div` 7 | display: flex; 8 | flex-direction: column; 9 | gap: 8px; 10 | `; 11 | export function InviteModal({ inviteData, ...props }: ModalProps<"invite">) { 12 | const splashUrl = REST.makeCDNUrl(`/splashes/${inviteData.guild?.id}/${inviteData.guild?.splash}.png`, { 13 | size: 2048, 14 | }); 15 | 16 | return ( 17 | 18 |
25 |
30 |
38 | 39 | You've been invited to join 40 | {inviteData.guild?.name} 41 | 42 |
43 |
44 |
53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import "@fontsource/roboto-mono/100.css"; 2 | import "@fontsource/roboto-mono/200.css"; 3 | import "@fontsource/roboto-mono/300.css"; 4 | import "@fontsource/roboto-mono/400.css"; 5 | import "@fontsource/roboto-mono/500.css"; 6 | import "@fontsource/roboto-mono/600.css"; 7 | import "@fontsource/roboto-mono/700.css"; 8 | import "@fontsource/roboto/100.css"; 9 | import "@fontsource/roboto/300.css"; 10 | import "@fontsource/roboto/400.css"; 11 | import "@fontsource/roboto/500.css"; 12 | import "@fontsource/roboto/700.css"; 13 | import "@fontsource/roboto/900.css"; 14 | 15 | import { ModalRenderer } from "@/controllers/modals"; 16 | import dayjs from "dayjs"; 17 | import calendar from "dayjs/plugin/calendar"; 18 | import relativeTime from "dayjs/plugin/relativeTime"; 19 | import ReactDOM from "react-dom/client"; 20 | import { BrowserRouter } from "react-router-dom"; 21 | import { ErrorBoundaryContext } from "react-use-error-boundary"; 22 | import App from "./App"; 23 | import { ContextMenuContextProvider } from "./contexts/ContextMenuContextProvider"; 24 | import Theme from "./contexts/Theme"; 25 | import "./index.css"; 26 | import { calendarStrings } from "./utils"; 27 | 28 | dayjs.extend(relativeTime); 29 | dayjs.extend(calendar, calendarStrings); 30 | 31 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | , 41 | ); 42 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1681202837, 9 | "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "cfacdce06f30d2b68473a46042957675eebb3401", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1682258674, 24 | "narHash": "sha256-nSqXN+dBgw8+DeE3vBhdHB7C2p+2QBDsA27ZptjJz/Y=", 25 | "owner": "lilyinstarlight", 26 | "repo": "nixpkgs", 27 | "rev": "47c77aa566f5114b4d759280702b9b910d3c7f0f", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "lilyinstarlight", 32 | "ref": "unheck/nodejs", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /src/stores/ReadStateStore.ts: -------------------------------------------------------------------------------- 1 | import type { APIReadState } from "@spacebarchat/spacebar-api-types/v9"; 2 | import { ReadState } from "@structures"; 3 | import { ObservableMap, action, computed, makeAutoObservable, observable } from "mobx"; 4 | import AppStore from "./AppStore"; 5 | 6 | export default class ReadStateStore { 7 | private readonly app: AppStore; 8 | @observable readonly readstates: ObservableMap; 9 | 10 | constructor(app: AppStore) { 11 | this.app = app; 12 | this.readstates = observable.map(); 13 | 14 | makeAutoObservable(this); 15 | } 16 | 17 | @action 18 | add(readstate: APIReadState) { 19 | this.readstates.set(readstate.id, new ReadState(this.app, readstate)); 20 | } 21 | 22 | @action 23 | update(readstate: APIReadState) { 24 | const existing = this.readstates.get(readstate.id); 25 | if (existing) { 26 | existing.update(readstate); 27 | } else { 28 | this.add(readstate); 29 | } 30 | } 31 | 32 | @action 33 | addAll(readstates: APIReadState[]) { 34 | readstates.forEach((readstate) => this.add(readstate)); 35 | } 36 | 37 | /** 38 | * Get a channels readstate 39 | * @param id channel id 40 | * @returns 41 | */ 42 | get(id: string) { 43 | return this.readstates.get(id); 44 | } 45 | 46 | @computed 47 | get all() { 48 | return Array.from(this.readstates.values()); 49 | } 50 | 51 | @action 52 | remove(id: string) { 53 | this.readstates.delete(id); 54 | } 55 | 56 | @computed 57 | get count() { 58 | return this.readstates.size; 59 | } 60 | 61 | has(id: string) { 62 | return this.readstates.has(id); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/HCaptcha.tsx: -------------------------------------------------------------------------------- 1 | import HCaptchaLib from "@hcaptcha/react-hcaptcha"; 2 | import React from "react"; 3 | import styled from "styled-components"; 4 | import { AuthContainer, Wrapper } from "./AuthComponents"; 5 | 6 | export const HeaderContainer = styled.div` 7 | width: 100%; 8 | `; 9 | 10 | export const Header = styled.h1` 11 | font-weight: var(--font-weight-bold); 12 | margin-bottom: 8px; 13 | font-size: 24px; 14 | color: var(--text); 15 | `; 16 | 17 | export const SubHeader = styled.h2` 18 | color: var(--text-muted); 19 | font-weight: var(--font-weight-regular); 20 | font-size: 16px; 21 | margin-bottom: 40px; 22 | `; 23 | 24 | interface Props { 25 | sitekey: string; 26 | captchaRef: React.RefObject; 27 | onLoad?: () => void; 28 | onChalExpired?: () => void; 29 | onError?: (e: unknown) => void; 30 | onExpire?: () => void; 31 | onVerify?: (token: string) => void; 32 | } 33 | 34 | function HCaptcha(props: Props) { 35 | return ( 36 | 37 | 38 | 39 |
Welcome Back!
40 | Beep boop. Boop beep? 41 | 42 | 52 |
53 |
54 |
55 | ); 56 | } 57 | 58 | export default HCaptcha; 59 | -------------------------------------------------------------------------------- /src/stores/PresenceStore.ts: -------------------------------------------------------------------------------- 1 | import type { GatewayPresenceUpdateDispatchData, Snowflake } from "@spacebarchat/spacebar-api-types/v9"; 2 | import { Presence } from "@structures"; 3 | import { action, computed, makeAutoObservable, observable, ObservableMap } from "mobx"; 4 | import AppStore from "./AppStore"; 5 | 6 | export default class PresenceStore { 7 | private readonly app: AppStore; 8 | @observable presences: ObservableMap; 9 | 10 | constructor(app: AppStore) { 11 | this.app = app; 12 | this.presences = observable.map(); 13 | 14 | makeAutoObservable(this); 15 | } 16 | 17 | @action 18 | add(data: GatewayPresenceUpdateDispatchData) { 19 | if (!this.presences.has(data.user.id)) { 20 | this.presences.set(data.user.id, new Presence(this.app, data)); 21 | } else { 22 | this.update(data); 23 | } 24 | } 25 | 26 | @action 27 | addAll(data: GatewayPresenceUpdateDispatchData[]) { 28 | data.forEach((p) => this.add(p)); 29 | } 30 | 31 | @computed 32 | get all() { 33 | return Array.from(this.presences.values()); 34 | } 35 | 36 | @action 37 | remove(id: Snowflake) { 38 | this.presences.delete(id); 39 | } 40 | 41 | @action 42 | update(data: GatewayPresenceUpdateDispatchData) { 43 | this.presences.get(data.user.id)?.update(data); 44 | } 45 | 46 | get(id: Snowflake) { 47 | return this.presences.get(id); 48 | } 49 | 50 | has(id: Snowflake) { 51 | return this.presences.has(id); 52 | } 53 | 54 | asList() { 55 | return Array.from(this.presences.values()); 56 | } 57 | 58 | get size() { 59 | return this.presences.size; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src-tauri/gen/apple/app_iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 0.1.1 19 | CFBundleVersion 20 | 0.1.1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | arm64 28 | metal 29 | 30 | UISupportedInterfaceOrientations 31 | 32 | UIInterfaceOrientationPortrait 33 | UIInterfaceOrientationLandscapeLeft 34 | UIInterfaceOrientationLandscapeRight 35 | 36 | UISupportedInterfaceOrientations~ipad 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationPortraitUpsideDown 40 | UIInterfaceOrientationLandscapeLeft 41 | UIInterfaceOrientationLandscapeRight 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | // inspired by revite: https://github.com/revoltchat/revite/blob/master/src/lib/ErrorBoundary.tsx 2 | 3 | import React from "react"; 4 | import { useErrorBoundary } from "react-use-error-boundary"; 5 | import styled from "styled-components"; 6 | import Button from "./Button"; 7 | 8 | const Container = styled.div` 9 | height: 100%; 10 | padding: 12px; 11 | background-color: var(--background-secondary); 12 | color: var(--text); 13 | 14 | h3 { 15 | margin: 0; 16 | margin-bottom: 10px; 17 | } 18 | 19 | code { 20 | font-size: 12px; 21 | } 22 | `; 23 | 24 | interface Props { 25 | children: React.ReactNode; 26 | section: "app" | "component"; 27 | } 28 | 29 | function ErrorBoundary({ children, section }: Props) { 30 | const [error, resetError] = useErrorBoundary(); 31 | 32 | // TODO: when v1 is reached, maybe we should add a "report" button here to submit errors to sentry 33 | 34 | if (error) { 35 | const message = error instanceof Error ? error.message : (error as string); 36 | const stack = error instanceof Error ? error.stack : undefined; 37 | return ( 38 | 39 | {section === "app" ? ( 40 | <> 41 |

App Crash

42 | 43 | 44 | 45 | ) : ( 46 | <> 47 |

Component Error

48 | 49 | 50 | )} 51 |
52 |
53 | {message} 54 |
55 | {stack} 56 |
57 | ); 58 | } 59 | 60 | return <>{children}; 61 | } 62 | 63 | export default ErrorBoundary; 64 | -------------------------------------------------------------------------------- /src/stores/objects/QueuedMessage.ts: -------------------------------------------------------------------------------- 1 | import { APIUser, MessageType } from "@spacebarchat/spacebar-api-types/v9"; 2 | import { AppStore } from "@stores"; 3 | import { action, observable } from "mobx"; 4 | import MessageBase from "./MessageBase"; 5 | 6 | export enum QueuedMessageStatus { 7 | SENDING = "sending", 8 | FAILED = "failed", 9 | } 10 | 11 | export type QueuedMessageData = { 12 | id: string; 13 | channel_id: string; 14 | guild_id?: string; 15 | content: string; 16 | files?: File[]; 17 | timestamp: string; 18 | type: MessageType; 19 | author: APIUser; 20 | }; 21 | 22 | export default class QueuedMessage extends MessageBase { 23 | channel_id: string; 24 | guild_id?: string; 25 | files?: File[]; 26 | @observable progress = 0; 27 | @observable status: QueuedMessageStatus; 28 | @observable error?: string; 29 | abortCallback?: () => void; 30 | 31 | constructor(app: AppStore, data: QueuedMessageData) { 32 | super(app, data); 33 | this.id = data.id; 34 | this.channel_id = data.channel_id; 35 | this.guild_id = data.guild_id; 36 | this.files = data.files; 37 | this.status = QueuedMessageStatus.SENDING; 38 | } 39 | 40 | @action 41 | updateProgress(e: ProgressEvent) { 42 | this.progress = Math.round((e.loaded / e.total) * 100); 43 | } 44 | 45 | @action 46 | setAbortCallback(cb: () => void) { 47 | this.abortCallback = cb; 48 | } 49 | 50 | abort() { 51 | if (this.abortCallback) { 52 | this.abortCallback(); 53 | } 54 | } 55 | 56 | @action 57 | /** 58 | * Mark this message as failed. 59 | */ 60 | fail(error: string) { 61 | this.error = error; 62 | this.status = QueuedMessageStatus.FAILED; 63 | } 64 | 65 | delete() { 66 | // 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/stores/UserStore.ts: -------------------------------------------------------------------------------- 1 | import useLogger from "@hooks/useLogger"; 2 | import { 3 | GatewayUserUpdateDispatchData, 4 | Routes, 5 | type APIUser, 6 | type Snowflake, 7 | } from "@spacebarchat/spacebar-api-types/v9"; 8 | import { User } from "@structures"; 9 | import { ObservableMap, action, computed, makeAutoObservable, observable } from "mobx"; 10 | import AppStore from "./AppStore"; 11 | 12 | export default class UserStore { 13 | private readonly logger = useLogger("UserStore"); 14 | @observable readonly users: ObservableMap; 15 | 16 | constructor(private readonly app: AppStore) { 17 | this.users = observable.map(); 18 | makeAutoObservable(this); 19 | } 20 | 21 | @action 22 | add(user: APIUser): User { 23 | const newUser = new User(user); 24 | this.users.set(user.id, newUser); 25 | return newUser; 26 | } 27 | 28 | @action 29 | addAll(users: APIUser[]) { 30 | users.forEach((user) => this.add(user)); 31 | } 32 | 33 | @action 34 | update(user: APIUser | GatewayUserUpdateDispatchData) { 35 | this.users.get(user.id)?.update(user); 36 | } 37 | 38 | @action 39 | get(id: string) { 40 | return this.users.get(id); 41 | } 42 | 43 | @computed 44 | get all() { 45 | return Array.from(this.users.values()); 46 | } 47 | 48 | @computed 49 | get count() { 50 | return this.users.size; 51 | } 52 | 53 | has(id: string) { 54 | return this.users.has(id); 55 | } 56 | 57 | @action 58 | async resolve(id: Snowflake, force: boolean = false): Promise { 59 | if (this.has(id) && !force) return this.get(id); 60 | const user = await this.app.rest.get(Routes.user(id)); 61 | if (!user) return undefined; 62 | return this.add(user); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/controllers/modals/types.ts: -------------------------------------------------------------------------------- 1 | // adapted from https://github.com/revoltchat/revite/blob/master/src/controllers/modals/types.ts 2 | 3 | import { 4 | APIAttachment, 5 | APIEmbedImage, 6 | APIEmbedThumbnail, 7 | APIEmbedVideo, 8 | APIInvite, 9 | } from "@spacebarchat/spacebar-api-types/v9"; 10 | import { Channel, Guild, GuildMember, Message } from "@structures"; 11 | 12 | export type Modal = { 13 | key?: string; 14 | } & ( 15 | | { 16 | type: "add_server" | "create_server" | "join_server" | "settings"; 17 | } 18 | | { 19 | type: "error"; 20 | title: string; 21 | description?: string; 22 | error: string; 23 | recoverable?: boolean; 24 | } 25 | | { 26 | type: "clipboard"; 27 | text: string; 28 | } 29 | | { 30 | type: "create_invite"; 31 | target: Channel; 32 | } 33 | | { 34 | type: "kick_member"; 35 | target: GuildMember; 36 | } 37 | | { 38 | type: "ban_member"; 39 | target: GuildMember; 40 | } 41 | | { 42 | type: "delete_message"; 43 | target: Message; 44 | } 45 | | { 46 | type: "leave_server"; 47 | target: Guild; 48 | } 49 | | { 50 | type: "image_viewer"; 51 | attachment: APIAttachment | APIEmbedImage | APIEmbedThumbnail | APIEmbedVideo; 52 | width?: number; 53 | height?: number; 54 | isVideo?: boolean; 55 | } 56 | | { 57 | type: "create_channel"; 58 | guild: Guild; 59 | category?: Channel; 60 | } 61 | | { 62 | type: "invite"; 63 | inviteData: APIInvite; 64 | } 65 | | { 66 | type: "text_viewer"; 67 | text: string; 68 | } 69 | ); 70 | 71 | export type ModalProps = Modal & { type: T } & { 72 | onClose: () => void; 73 | signal?: "close" | "confirm"; 74 | }; 75 | -------------------------------------------------------------------------------- /src/components/MemberList/MemberList.tsx: -------------------------------------------------------------------------------- 1 | import ListSection from "@components/ListSection"; 2 | import { useAppStore } from "@hooks/useAppStore"; 3 | import { GuildMemberListStore } from "@stores"; 4 | import { autorun } from "mobx"; 5 | import { observer } from "mobx-react-lite"; 6 | import React from "react"; 7 | import styled from "styled-components"; 8 | import MemberListItem from "./MemberListItem"; 9 | 10 | const Container = styled.div` 11 | display: flex; 12 | flex: 0 0 240px; 13 | flex-direction: column; 14 | background-color: var(--background-secondary); 15 | overflow-x: hidden; 16 | 17 | @media (max-width: 1050px) { 18 | display: none; 19 | } 20 | `; 21 | 22 | const List = styled.ul` 23 | padding: 0; 24 | margin: 0; 25 | list-style: none; 26 | // overflow-y: auto; 27 | // height: 100%; 28 | // width: 100%; 29 | `; 30 | 31 | function MemberList() { 32 | const app = useAppStore(); 33 | const [list, setList] = React.useState(null); 34 | 35 | React.useEffect( 36 | () => 37 | autorun(() => { 38 | if (app.activeGuild && app.activeChannel) { 39 | const { memberLists } = app.activeGuild; 40 | const listId = app.activeChannel.listId; 41 | const store = memberLists.get(listId); 42 | setList(store ? store.list : null); 43 | } else { 44 | setList(null); 45 | } 46 | }), 47 | [], 48 | ); 49 | 50 | return ( 51 | 52 | 53 | {list 54 | ? list.map((category, i) => ( 55 | ( 59 | 60 | ))} 61 | /> 62 | )) 63 | : null} 64 | 65 | 66 | ); 67 | } 68 | 69 | export default observer(MemberList); 70 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "productName": "Spacebar", 3 | "version": "./version.json", 4 | "identifier": "chat.spacebar.app", 5 | "build": { 6 | "frontendDist": "../dist", 7 | "devUrl": "http://localhost:1420", 8 | "beforeDevCommand": "pnpm run dev", 9 | "beforeBuildCommand": "pnpm run build" 10 | }, 11 | "app": { 12 | "withGlobalTauri": true, 13 | "windows": [ 14 | { 15 | "label": "main", 16 | "title": "Tauri", 17 | "width": 800, 18 | "height": 600, 19 | "visible": false 20 | }, 21 | { 22 | "label": "splashscreen", 23 | "width": 400, 24 | "height": 200, 25 | "decorations": false, 26 | "resizable": false, 27 | "url": "splashscreen.html" 28 | } 29 | ] 30 | }, 31 | "bundle": { 32 | "active": true, 33 | "targets": ["deb", "rpm", "appimage", "nsis", "app", "dmg"], 34 | "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"], 35 | "publisher": "Spacebar", 36 | "category": "SocialNetworking", 37 | "shortDescription": "A free, opensource self-hostable discord-compatible chat, voice and video platform.", 38 | "licenseFile": "../LICENSE", 39 | "windows": { 40 | "nsis": { 41 | "sidebarImage": "./icons/sidebar.bmp", 42 | "installerIcon": "./icons/icon.ico" 43 | } 44 | }, 45 | "createUpdaterArtifacts": true 46 | }, 47 | "plugins": { 48 | "shell": { 49 | "open": true 50 | }, 51 | "updater": { 52 | "active": true, 53 | "endpoints": ["https://github.com/spacebarchat/client/releases/download/latest/latest.json"], 54 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDQxRkQwNTY1NzBEOTMyMTUKUldRVk10bHdaUVg5UWVoVm9JeDg4UEs1TkpMT3FKdzc3Y29CN2NZNk9vRE9sanJCUERqT09HVVYK", 55 | "windows": { 56 | "installMode": "passive" 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/markdown/components/Spoiler.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Container = styled.span` 5 | background-color: hsl(var(--background-tertiary-hsl)); 6 | padding: 0 5px; 7 | border-radius: 4px; 8 | width: fit-content; 9 | transition: background-color 0.1s ease; 10 | 11 | &:hover { 12 | background-color: hsl(var(--background-tertiary-hsl) / 0.3); 13 | cursor: pointer; 14 | } 15 | 16 | &.visible { 17 | background-color: hsl(var(--background-tertiary-hsl) / 0.3); 18 | cursor: pointer; 19 | } 20 | 21 | .text { 22 | opacity: 0; 23 | transition: opacity 0.1s ease; 24 | } 25 | 26 | &.visible .text { 27 | opacity: 1; 28 | } 29 | `; 30 | 31 | interface Props { 32 | children: React.ReactNode; 33 | } 34 | 35 | function Spoiler({ children }: Props) { 36 | const [shown, setShown] = useState(false); 37 | const containerRef = useRef(null); 38 | 39 | const show = () => setShown(true); 40 | 41 | useEffect(() => { 42 | const handleIntersection = (entries: IntersectionObserverEntry[]) => { 43 | // when the element is not in the viewport, hide the spoiler 44 | if (entries[0].intersectionRatio === 0 && shown) { 45 | setShown(false); 46 | } 47 | }; 48 | 49 | const observer = new IntersectionObserver(handleIntersection); 50 | 51 | if (containerRef.current) { 52 | observer.observe(containerRef.current); 53 | } 54 | 55 | return () => { 56 | if (containerRef.current) { 57 | observer.unobserve(containerRef.current); 58 | } 59 | }; 60 | }, [shown]); 61 | 62 | return ( 63 | 64 | {children} 65 | 66 | ); 67 | } 68 | 69 | export default Spoiler; 70 | -------------------------------------------------------------------------------- /src/components/media/File.tsx: -------------------------------------------------------------------------------- 1 | import { APIAttachment } from "@spacebarchat/spacebar-api-types/v9"; 2 | import { bytesToSize } from "@utils"; 3 | import styled from "styled-components"; 4 | import Icon from "../Icon"; 5 | import Link from "../Link"; 6 | 7 | const Container = styled.div` 8 | margin-top: 10px; 9 | display: flex; 10 | background-color: var(--background-secondary); 11 | padding: 12px; 12 | border-radius: 5px; 13 | flex: auto; 14 | border: 1px solid var(--background-secondary-alt); 15 | justify-content: space-between; 16 | box-sizing: border-box; 17 | flex-direction: column; 18 | min-width: 300px; 19 | width: 420px; 20 | 21 | @media only screen and (max-width: 420px) { 22 | width: 100%; 23 | } 24 | `; 25 | 26 | const FileInfo = styled.div` 27 | display: flex; 28 | `; 29 | 30 | const FileMetadata = styled.div` 31 | white-space: nowrap; 32 | overflow: hidden; 33 | text-overflow: ellipsis; 34 | align-self: center; 35 | color: var(--text-link); 36 | `; 37 | 38 | const FileSize = styled.div` 39 | color: var(--text-secondary); 40 | font-size: 12px; 41 | opacity: 0.8; 42 | font-weight: var(--font-weight-medium); 43 | text-overflow: ellipsis; 44 | overflow: hidden; 45 | `; 46 | 47 | interface Props { 48 | attachment: APIAttachment; 49 | } 50 | 51 | export function File({ attachment }: Props) { 52 | const url = attachment.proxy_url && attachment.proxy_url.length > 0 ? attachment.proxy_url : attachment.url; 53 | 54 | return ( 55 | 56 | 57 | 58 | 59 | 60 | {attachment.filename} 61 | 62 | {bytesToSize(attachment.size)} 63 | 64 | 65 | 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /src/pages/InvitePage.tsx: -------------------------------------------------------------------------------- 1 | import Container from "@components/Container"; 2 | import { InviteModal, InviteUnauthedModal } from "@components/modals"; 3 | import { useAppStore } from "@hooks/useAppStore"; 4 | import { APIInvite, Routes } from "@spacebarchat/spacebar-api-types/v9"; 5 | import { REST } from "@utils"; 6 | import React, { useEffect } from "react"; 7 | import { useParams } from "react-router-dom"; 8 | 9 | function InvitePage() { 10 | const app = useAppStore(); 11 | const [inviteData, setInviteData] = React.useState(); 12 | const [error, setError] = React.useState(); 13 | const { code } = useParams<{ code: string }>(); 14 | 15 | useEffect(() => { 16 | if (!code) { 17 | setError("No invite code provided"); 18 | return; 19 | } 20 | 21 | console.log("Invite code: ", code); 22 | 23 | // fetch invite data 24 | app.rest 25 | .get(Routes.invite(code)) 26 | .then((data) => setInviteData(data)) 27 | .catch((e) => setError(e.message)); 28 | }, []); 29 | 30 | if (error) { 31 | return ( 32 | 33 |

Error

34 |

{error}

35 |
36 | ); 37 | } 38 | 39 | if (!inviteData) { 40 | return ( 41 | 42 |

Loading...

43 |
44 | ); 45 | } 46 | 47 | const splashUrl = REST.makeCDNUrl(`/splashes/${inviteData.guild?.id}/${inviteData.guild?.splash}.png`); 48 | 49 | return ( 50 | 57 | {app.token ? ( 58 | {}} /> 59 | ) : ( 60 | {}} /> 61 | )} 62 | 63 | ); 64 | } 65 | 66 | export default InvitePage; 67 | -------------------------------------------------------------------------------- /src/components/IconButton.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | interface Props { 4 | // variant?: "primary" | "secondary" | "danger" | "success" | "warning"; 5 | variant?: "filled" | "outlined" | "blank"; 6 | color?: string; 7 | } 8 | 9 | export default styled.button` 10 | position: relative; 11 | margin: 0; 12 | padding: 0; 13 | width: 32px; 14 | height: 32px; 15 | cursor: pointer; 16 | outline: none; 17 | opacity: ${(props) => (props.disabled ? 0.5 : 1)}; 18 | background-color: transparent; 19 | 20 | color: ${(props) => { 21 | if (props.variant === "outlined") return "transparent"; 22 | // switch (props.variant) { 23 | // case "primary": 24 | // return "var(--primary)"; 25 | // case "secondary": 26 | // return "var(--secondary)"; 27 | // case "danger": 28 | // return "var(--danger)"; 29 | // case "success": 30 | // return "var(--success)"; 31 | // case "warning": 32 | // return "var(--warning)"; 33 | // default: 34 | // return "var(--primary)"; 35 | // } 36 | return props.color; 37 | }}; 38 | 39 | border: ${(props) => { 40 | if (props.variant !== "outlined") return "none"; 41 | // switch (props.variant) { 42 | // case "primary": 43 | // return "1px solid var(--primary)"; 44 | // case "secondary": 45 | // return "1px solid var(--secondary)"; 46 | // case "danger": 47 | // return "1px solid var(--danger)"; 48 | // case "success": 49 | // return "1px solid var(--success)"; 50 | // case "warning": 51 | // return "1px solid var(--warning)"; 52 | // default: 53 | // return "1px solid var(--primary)"; 54 | // } 55 | return props.color; 56 | }}; 57 | 58 | &:hover { 59 | background-color: ${(props) => (props.variant === "blank" ? "transparent" : "var(--background-secondary)")} 60 | cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; 61 | } 62 | `; 63 | -------------------------------------------------------------------------------- /.github/workflows/tauri.yml: -------------------------------------------------------------------------------- 1 | name: "tauri publish" 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | publish-tauri: 7 | permissions: 8 | contents: write 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | platform: [macos-latest, ubuntu-latest, windows-latest] 13 | 14 | runs-on: ${{ matrix.platform }} 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: pnpm/action-setup@v2 18 | with: 19 | version: latest 20 | - name: Setup Node 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 19 24 | cache: "pnpm" 25 | - name: Install Rust stable 26 | uses: dtolnay/rust-toolchain@stable 27 | - name: Install Dependencies (Ubuntu only) 28 | if: matrix.platform == 'ubuntu-latest' 29 | run: | 30 | sudo apt-get update 31 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libsoup-3.0-dev libjavascriptcoregtk-4.1-dev 32 | - name: Install Frontend Dependencies 33 | run: pnpm i 34 | - name: Run version generator 35 | run: pnpm run ci:prebuild 36 | - uses: spacebarchat/tauri-action@dev 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} 40 | TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} 41 | with: 42 | tagName: client-__BRANCH__-v__VERSION__ 43 | releaseName: "Spacebar Client v__VERSION__ (__BRANCH__)" 44 | releaseBody: "See the assets to download this version and install. The current commit is __SHA__." 45 | releaseDraft: false 46 | prerelease: true 47 | includeDebug: true 48 | includeRelease: true 49 | includeUpdaterJson: true 50 | -------------------------------------------------------------------------------- /src/components/messaging/MessageTextArea.tsx: -------------------------------------------------------------------------------- 1 | import { isTouchscreenDevice } from "@utils"; 2 | import React, { useEffect } from "react"; 3 | import styled from "styled-components"; 4 | import ContentEditableInput from "../ContentEditableInput"; 5 | 6 | const Container = styled.div` 7 | flex: 1; 8 | display: flex; 9 | `; 10 | 11 | interface Props { 12 | id: string; 13 | value: string; 14 | onChange: (value: string) => void; 15 | onKeyDown?: (e: React.KeyboardEvent) => void; 16 | placeholder?: string; 17 | disabled?: boolean; 18 | maxLength?: number; 19 | } 20 | 21 | function MessageTextArea({ id, value, onChange, onKeyDown, placeholder, disabled, maxLength }: Props) { 22 | const ref = React.useRef(null); 23 | 24 | useEffect(() => { 25 | if (isTouchscreenDevice) return; 26 | if (ref.current) ref.current.focus(); 27 | }, [value]); 28 | 29 | const inputSelected = () => ["TEXTAREA", "INPUT", "DIV"].includes(document.activeElement?.nodeName ?? ""); 30 | 31 | useEffect(() => { 32 | if (!ref.current) return; 33 | 34 | if (isTouchscreenDevice) return; 35 | if (!inputSelected()) { 36 | ref.current.focus(); 37 | } 38 | 39 | function keyDown(e: KeyboardEvent) { 40 | if ((e.ctrlKey && e.key !== "v") || e.altKey || e.metaKey) return; 41 | if (e.key.length !== 1) return; 42 | if (ref && !inputSelected()) { 43 | ref.current!.focus(); 44 | } 45 | } 46 | 47 | document.body.addEventListener("keydown", keyDown); 48 | return () => document.body.removeEventListener("keydown", keyDown); 49 | }, [ref, value]); 50 | 51 | return ( 52 | 53 | 62 | 63 | ); 64 | } 65 | 66 | export default MessageTextArea; 67 | -------------------------------------------------------------------------------- /src/components/markdown/components/Codeblock.tsx: -------------------------------------------------------------------------------- 1 | // adapted from Revite 2 | // https://github.com/revoltchat/revite/blob/fe63c6633f32b54aa1989cb34627e72bb3377efd/src/components/markdown/plugins/Codeblock.tsx 3 | 4 | import Floating from "@components/floating/Floating"; 5 | import FloatingTrigger from "@components/floating/FloatingTrigger"; 6 | import React from "react"; 7 | import styled from "styled-components"; 8 | 9 | const Actions = styled.div` 10 | position: absolute; 11 | top: 10px; 12 | right: 10px; 13 | display: flex; 14 | gap: 5px; 15 | 16 | a { 17 | color: var(--text); 18 | cursor: pointer; 19 | padding: 2px 6px; 20 | font-weight: 600; 21 | user-select: none; 22 | display: inline-block; 23 | background: var(--background-tertiary); 24 | 25 | font-size: 10px; 26 | text-transform: uppercase; 27 | } 28 | `; 29 | 30 | interface Props { 31 | class?: string; 32 | lang?: string; 33 | children: React.ReactNode; 34 | } 35 | 36 | /** 37 | * Render a codeblock with copy text button 38 | */ 39 | 40 | function CodeBlock(props: Props) { 41 | const ref = React.useRef(null); 42 | 43 | let text = "Copy"; 44 | if (props.class) { 45 | text = props.class.split("-")[1]; 46 | } 47 | 48 | const onCopy = React.useCallback(() => { 49 | const text = ref.current?.querySelector("code")?.innerText; 50 | if (text) navigator.clipboard.writeText(text); 51 | }, [ref]); 52 | 53 | return ( 54 |
60 | 			
61 | 				"Copy to Clipboard,
66 | 					}}
67 | 				>
68 | 					
69 | 						{text}
70 | 					
71 | 				
72 | 			
73 | 			{props.children}
74 | 		
75 | ); 76 | } 77 | 78 | export default CodeBlock; 79 | -------------------------------------------------------------------------------- /src/components/contextMenus/MessageContextMenu.tsx: -------------------------------------------------------------------------------- 1 | import { modalController } from "@/controllers/modals"; 2 | import { useAppStore } from "@hooks/useAppStore"; 3 | import { Message } from "@structures"; 4 | import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "./ContextMenu"; 5 | 6 | interface MenuProps { 7 | message: Message; 8 | } 9 | 10 | function MessageContextMenu({ message }: MenuProps) { 11 | const app = useAppStore(); 12 | 13 | function copyRaw() { 14 | navigator.clipboard.writeText(message.content); 15 | } 16 | 17 | async function deleteMessage(e: MouseEvent) { 18 | if (e.shiftKey) { 19 | await message.delete(); 20 | } else { 21 | modalController.push({ 22 | type: "delete_message", 23 | target: message as Message, 24 | }); 25 | } 26 | } 27 | 28 | function copyId() { 29 | navigator.clipboard.writeText(message.id); 30 | } 31 | 32 | return ( 33 | 34 | 35 | Reply 36 | 37 | 38 | Copy Raw Text 39 | 40 | 41 | {((message instanceof Message && message.channel.hasPermission("MANAGE_MESSAGES")) || 42 | message.author.id === app.account?.id) && 43 | message instanceof Message && ( 44 | <> 45 | 46 | Delete Message 47 | 48 | 49 | 50 | )} 51 | 62 | Copy Message ID 63 | 64 | 65 | ); 66 | } 67 | 68 | export default MessageContextMenu; 69 | -------------------------------------------------------------------------------- /src-tauri/gen/android/buildSrc/src/main/java/chat/spacebar/app/kotlin/BuildTask.kt: -------------------------------------------------------------------------------- 1 | import java.io.File 2 | import org.apache.tools.ant.taskdefs.condition.Os 3 | import org.gradle.api.DefaultTask 4 | import org.gradle.api.GradleException 5 | import org.gradle.api.logging.LogLevel 6 | import org.gradle.api.tasks.Input 7 | import org.gradle.api.tasks.TaskAction 8 | 9 | open class BuildTask : DefaultTask() { 10 | @Input 11 | var rootDirRel: String? = null 12 | @Input 13 | var target: String? = null 14 | @Input 15 | var release: Boolean? = null 16 | 17 | @TaskAction 18 | fun assemble() { 19 | val executable = """node"""; 20 | try { 21 | runTauriCli(executable) 22 | } catch (e: Exception) { 23 | if (Os.isFamily(Os.FAMILY_WINDOWS)) { 24 | runTauriCli("$executable.cmd") 25 | } else { 26 | throw e; 27 | } 28 | } 29 | } 30 | 31 | fun runTauriCli(executable: String) { 32 | val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null") 33 | val target = target ?: throw GradleException("target cannot be null") 34 | val release = release ?: throw GradleException("release cannot be null") 35 | val args = listOf("..\\node_modules\\.bin\\\\..\\@tauri-apps\\cli\\tauri.js", "android", "android-studio-script"); 36 | 37 | project.exec { 38 | workingDir(File(project.projectDir, rootDirRel)) 39 | executable(executable) 40 | args(args) 41 | if (project.logger.isEnabled(LogLevel.DEBUG)) { 42 | args("-vv") 43 | } else if (project.logger.isEnabled(LogLevel.INFO)) { 44 | args("-v") 45 | } 46 | if (release) { 47 | args("--release") 48 | } 49 | args(listOf("--target", target)) 50 | }.assertNormalExitValue() 51 | } 52 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | Spacebar 38 | 39 | 40 | 41 |
42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src-tauri/gen/android/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | id("rust") 5 | } 6 | 7 | android { 8 | compileSdk = 33 9 | namespace = "chat.spacebar.app" 10 | defaultConfig { 11 | manifestPlaceholders["usesCleartextTraffic"] = "false" 12 | applicationId = "chat.spacebar.app" 13 | minSdk = 24 14 | targetSdk = 33 15 | versionCode = 1 16 | versionName = "1.0" 17 | } 18 | buildTypes { 19 | getByName("debug") { 20 | manifestPlaceholders["usesCleartextTraffic"] = "true" 21 | isDebuggable = true 22 | isJniDebuggable = true 23 | isMinifyEnabled = false 24 | packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so") 25 | jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so") 26 | jniLibs.keepDebugSymbols.add("*/x86/*.so") 27 | jniLibs.keepDebugSymbols.add("*/x86_64/*.so") 28 | } 29 | } 30 | getByName("release") { 31 | isMinifyEnabled = true 32 | proguardFiles( 33 | *fileTree(".") { include("**/*.pro") } 34 | .plus(getDefaultProguardFile("proguard-android-optimize.txt")) 35 | .toList().toTypedArray() 36 | ) 37 | } 38 | } 39 | kotlinOptions { 40 | jvmTarget = "1.8" 41 | } 42 | } 43 | 44 | rust { 45 | rootDirRel = "../../../" 46 | } 47 | 48 | dependencies { 49 | implementation("androidx.webkit:webkit:1.6.1") 50 | implementation("androidx.appcompat:appcompat:1.6.1") 51 | implementation("com.google.android.material:material:1.8.0") 52 | testImplementation("junit:junit:4.13.2") 53 | androidTestImplementation("androidx.test.ext:junit:1.1.4") 54 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") 55 | } 56 | 57 | apply(from = "tauri.build.gradle.kts") 58 | -------------------------------------------------------------------------------- /src/components/contextMenus/ChannelContextMenu.tsx: -------------------------------------------------------------------------------- 1 | // loosely based on https://github.com/revoltchat/frontend/blob/master/components/app/menus/UserContextMenu.tsx 2 | 3 | import { modalController } from "@/controllers/modals"; 4 | import Channel from "@structures/Channel"; 5 | import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "./ContextMenu"; 6 | 7 | interface MenuProps { 8 | channel: Channel; 9 | } 10 | 11 | function ChannelContextMenu({ channel }: MenuProps) { 12 | /** 13 | * Copy id to clipboard 14 | */ 15 | function copyId() { 16 | navigator.clipboard.writeText(channel.id); 17 | } 18 | 19 | /** 20 | * Copy link to clipboard 21 | */ 22 | function copyLink() { 23 | navigator.clipboard.writeText(`${window.location.origin}/channels/${channel.guildId}/${channel.id}`); 24 | } 25 | 26 | /** 27 | * Open invite creation modal 28 | */ 29 | function openInviteCreateModal() { 30 | modalController.push({ 31 | type: "create_invite", 32 | target: channel, 33 | }); 34 | } 35 | 36 | return ( 37 | 38 | 39 | Create Invite 40 | 41 | 42 | Copy Link 43 | 44 | 45 | {channel.hasPermission("MANAGE_CHANNELS") && ( 46 | <> 47 | Edit Channel 48 | 49 | Delete Channel 50 | 51 | 52 | 53 | )} 54 | 55 | 66 | Copy Channel ID 67 | 68 | 69 | ); 70 | } 71 | 72 | export default ChannelContextMenu; 73 | -------------------------------------------------------------------------------- /src/components/media/Audio.tsx: -------------------------------------------------------------------------------- 1 | import Icon from "@components/Icon"; 2 | import Link from "@components/Link"; 3 | import { APIAttachment } from "@spacebarchat/spacebar-api-types/v9"; 4 | import { bytesToSize } from "@utils"; 5 | import styled from "styled-components"; 6 | 7 | const Container = styled.div` 8 | margin-top: 10px; 9 | display: flex; 10 | background-color: var(--background-secondary); 11 | padding: 12px; 12 | border-radius: 5px; 13 | flex: auto; 14 | border: 1px solid var(--background-secondary-alt); 15 | justify-content: space-between; 16 | box-sizing: border-box; 17 | flex-direction: column; 18 | min-width: 300px; 19 | width: 420px; 20 | 21 | @media only screen and (max-width: 420px) { 22 | width: 100%; 23 | } 24 | `; 25 | 26 | const AudioInfo = styled.div` 27 | display: flex; 28 | `; 29 | 30 | const AudioMetadata = styled.div` 31 | white-space: nowrap; 32 | overflow: hidden; 33 | text-overflow: ellipsis; 34 | align-self: center; 35 | flex-direction: column; 36 | `; 37 | 38 | const AudioSize = styled.div` 39 | color: var(--text-secondary); 40 | font-size: 12px; 41 | opacity: 0.8; 42 | font-weight: var(--font-weight-medium); 43 | text-overflow: ellipsis; 44 | overflow: hidden; 45 | `; 46 | 47 | interface Props { 48 | attachment: APIAttachment; 49 | } 50 | 51 | export function Audio({ attachment }: Props) { 52 | const url = attachment.proxy_url && attachment.proxy_url.length > 0 ? attachment.proxy_url : attachment.url; 53 | 54 | return ( 55 | 56 | 57 | 58 | 59 | 60 | {attachment.filename} 61 | 62 | {bytesToSize(attachment.size)} 63 | 64 | 65 | 68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/components/modals/LeaveServerModal.tsx: -------------------------------------------------------------------------------- 1 | import { ModalProps, modalController } from "@/controllers/modals"; 2 | import { useAppStore } from "@hooks/useAppStore"; 3 | import useLogger from "@hooks/useLogger"; 4 | import { Routes } from "@spacebarchat/spacebar-api-types/v9"; 5 | import { useState } from "react"; 6 | import { useNavigate } from "react-router-dom"; 7 | import { Modal } from "./ModalComponents"; 8 | 9 | export function LeaveServerModal({ target, ...props }: ModalProps<"leave_server">) { 10 | const app = useAppStore(); 11 | const logger = useLogger("LeaveServerModal"); 12 | const navigate = useNavigate(); 13 | const [isDisabled, setDisabled] = useState(false); 14 | 15 | async function leaveGuild() { 16 | setDisabled(true); 17 | await app.rest 18 | .delete(Routes.userGuild(target.id)) 19 | .then(() => { 20 | navigate("/channels/@me"); 21 | modalController.pop("close"); 22 | }) 23 | .catch((e) => { 24 | logger.error(e); 25 | modalController.pop("close"); 26 | modalController.push({ 27 | type: "error", 28 | error: e, 29 | title: "Failed to leave server", 30 | description: "An error occurred while trying to leave the server.", 31 | }); 32 | }) 33 | .finally(() => setDisabled(false)); 34 | } 35 | 36 | return ( 37 | 42 | Are you sure you want to leave {target.name}? You won't be able to rejoin this guild unless 43 | you are re-invited. 44 | 45 | } 46 | actions={[ 47 | { 48 | onClick: leaveGuild, 49 | children: Leave Server, 50 | palette: "danger", 51 | confirmation: true, 52 | disabled: isDisabled, 53 | size: "small", 54 | }, 55 | { 56 | onClick: () => modalController.pop("close"), 57 | children: Cancel, 58 | palette: "link", 59 | disabled: isDisabled, 60 | size: "small", 61 | }, 62 | ]} 63 | /> 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/hooks/useFloating.tsx: -------------------------------------------------------------------------------- 1 | import { FloatingOptions } from "@components/floating/Floating"; 2 | import { 3 | arrow, 4 | autoUpdate, 5 | flip, 6 | offset, 7 | shift, 8 | useClick, 9 | useDismiss, 10 | useFloating, 11 | useFocus, 12 | useHover, 13 | useInteractions, 14 | useRole, 15 | } from "@floating-ui/react"; 16 | import { useMemo, useRef, useState } from "react"; 17 | 18 | export default function ({ 19 | type, 20 | initialOpen = false, 21 | offset: offsetMiddlewareOffset, 22 | placement, 23 | open: controlledOpen, 24 | onOpenChange: setControlledOpen, 25 | }: Omit) { 26 | const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen); 27 | const arrowRef = useRef(null); 28 | 29 | const open = controlledOpen ?? uncontrolledOpen; 30 | const setOpen = setControlledOpen ?? setUncontrolledOpen; 31 | 32 | const data = useFloating({ 33 | placement: placement, 34 | open, 35 | onOpenChange: setOpen, 36 | whileElementsMounted: autoUpdate, 37 | middleware: [ 38 | offset(type === "tooltip" && !offsetMiddlewareOffset ? 10 : offsetMiddlewareOffset ?? 5), 39 | flip(), 40 | shift({ 41 | padding: 8, 42 | }), 43 | arrow({ 44 | element: arrowRef, 45 | padding: 4, 46 | }), 47 | ], 48 | }); 49 | 50 | const context = data.context; 51 | 52 | const click = useClick(context, { 53 | enabled: type !== "tooltip", 54 | }); 55 | const dismiss = useDismiss(context); 56 | const role = useRole(context, { 57 | role: type === "tooltip" ? "tooltip" : undefined, 58 | }); 59 | 60 | const hover = useHover(context, { 61 | move: false, 62 | enabled: type == "tooltip", 63 | }); 64 | const focus = useFocus(context, { 65 | enabled: type == "tooltip", 66 | }); 67 | 68 | const interactions = useInteractions([click, dismiss, role, hover, focus]); 69 | 70 | return useMemo( 71 | () => ({ 72 | open, 73 | setOpen, 74 | ...interactions, 75 | ...data, 76 | arrowRef, 77 | }), 78 | [open, setOpen, interactions, data], 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /src/components/ChannelHeader.tsx: -------------------------------------------------------------------------------- 1 | import { Floating, FloatingTrigger } from "@components/floating"; 2 | import { useAppStore } from "@hooks/useAppStore"; 3 | import { observer } from "mobx-react-lite"; 4 | import React, { useEffect } from "react"; 5 | import styled from "styled-components"; 6 | import Icon, { IconProps } from "./Icon"; 7 | import { SectionHeader } from "./SectionHeader"; 8 | 9 | const Wrapper = styled(SectionHeader)` 10 | background-color: var(--background-secondary); 11 | cursor: pointer; 12 | 13 | &:hover { 14 | background-color: var(--background-secondary-highlight); 15 | } 16 | `; 17 | 18 | const HeaderText = styled.header` 19 | font-size: 16px; 20 | font-weight: var(--font-weight-medium); 21 | overflow: hidden; 22 | white-space: nowrap; 23 | text-overflow: ellipsis; 24 | user-select: none; 25 | `; 26 | 27 | function ChannelHeader() { 28 | const app = useAppStore(); 29 | 30 | const [isOpen, setOpen] = React.useState(false); 31 | const [icon, setIcon] = React.useState("mdiChevronDown"); 32 | 33 | const onOpenChange = (open: boolean) => { 34 | setOpen(open); 35 | }; 36 | 37 | useEffect(() => { 38 | if (isOpen) setIcon("mdiClose"); 39 | else setIcon("mdiChevronDown"); 40 | }, [isOpen]); 41 | 42 | if (app.activeGuildId === "@me") { 43 | return ( 44 | 52 | Direct Messages 53 | 54 | ); 55 | } 56 | 57 | if (!app.activeGuild) return null; 58 | 59 | return ( 60 | 61 | 62 | 63 | {app.activeGuild.name} 64 | 65 | 66 | 67 | 68 | ); 69 | } 70 | 71 | export default observer(ChannelHeader); 72 | -------------------------------------------------------------------------------- /src/components/ChannelList/ChannelList.tsx: -------------------------------------------------------------------------------- 1 | import { useAppStore } from "@hooks/useAppStore"; 2 | import { ChannelType } from "@spacebarchat/spacebar-api-types/v9"; 3 | import { observer } from "mobx-react-lite"; 4 | import { AutoSizer, List, ListRowProps } from "react-virtualized"; 5 | import styled from "styled-components"; 6 | import ChannelListItem from "./ChannelListItem"; 7 | 8 | const Container = styled.div` 9 | display: flex; 10 | flex: 1; 11 | `; 12 | 13 | function ChannelList() { 14 | const app = useAppStore(); 15 | 16 | if (!app.activeGuild || !app.activeChannel) return ; 17 | const guildId = app.activeGuild.id; 18 | 19 | const visibleChannels = app.channels.getVisibleChannelsForGuild(guildId); 20 | 21 | const toggleCategory = (categoryId: string) => { 22 | app.channels.toggleCategoryCollapse(guildId, categoryId); 23 | }; 24 | 25 | const rowRenderer = ({ index, key, style }: ListRowProps) => { 26 | const item = visibleChannels[index]; 27 | const active = app.activeChannelId === item.id; 28 | const isCategory = item.type === ChannelType.GuildCategory; 29 | 30 | return ( 31 |
32 | toggleCategory(item.id) : undefined} 39 | /> 40 |
41 | ); 42 | }; 43 | 44 | return ( 45 | 46 | 47 | {({ width, height }) => ( 48 | { 53 | const item = visibleChannels[index]; 54 | if (item.type === ChannelType.GuildCategory) { 55 | return 44; 56 | } 57 | return 33; 58 | }} 59 | rowRenderer={rowRenderer} 60 | width={width} 61 | /> 62 | )} 63 | 64 | 65 | ); 66 | } 67 | 68 | export default observer(ChannelList); 69 | -------------------------------------------------------------------------------- /src/hooks/useInstanceValidation.ts: -------------------------------------------------------------------------------- 1 | import useLogger from "@hooks/useLogger"; 2 | import { Globals, REST, RouteSettings } from "@utils"; 3 | import React, { useEffect } from "react"; 4 | import { FieldPath, FieldValues, UseFormClearErrors, UseFormSetError } from "react-hook-form"; 5 | 6 | const getValidURL = (url: string) => { 7 | try { 8 | return new URL(url); 9 | } catch (e) { 10 | return undefined; 11 | } 12 | }; 13 | 14 | export function useInstanceValidation( 15 | setError: UseFormSetError, 16 | clearErrors: UseFormClearErrors, 17 | instanceField: FieldPath = "instance" as FieldPath, 18 | ) { 19 | const logger = useLogger("InstanceValidation"); 20 | const [debounce, setDebounce] = React.useState(null); 21 | const [isCheckingInstance, setCheckingInstance] = React.useState(false); 22 | 23 | const handleInstanceChange = React.useCallback( 24 | (e: React.ChangeEvent) => { 25 | clearErrors(instanceField); 26 | setCheckingInstance(false); 27 | 28 | // Clear existing debounce 29 | if (debounce) clearTimeout(debounce); 30 | 31 | const doRequest = async () => { 32 | const url = getValidURL(e.target.value); 33 | if (!url) return; 34 | setCheckingInstance(true); 35 | 36 | let endpoints: RouteSettings; 37 | try { 38 | endpoints = await REST.getEndpointsFromDomain(url); 39 | } catch (e) { 40 | setCheckingInstance(false); 41 | return setError(instanceField, { 42 | type: "manual", 43 | message: (e instanceof Error && e.message) || "Instance could not be resolved", 44 | } as any); 45 | } 46 | 47 | logger.debug(`Instance lookup has set routes to`, endpoints); 48 | Globals.routeSettings = endpoints; 49 | Globals.save(); 50 | setCheckingInstance(false); 51 | }; 52 | 53 | setDebounce(setTimeout(doRequest, 500)); 54 | }, 55 | [debounce, setError, clearErrors, instanceField, logger], 56 | ); 57 | 58 | // Cleanup debounce on unmount 59 | useEffect(() => { 60 | return () => { 61 | if (debounce) clearTimeout(debounce); 62 | }; 63 | }, [debounce]); 64 | 65 | return { 66 | handleInstanceChange, 67 | isCheckingInstance, 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /src/components/contextMenus/ContextMenu.tsx: -------------------------------------------------------------------------------- 1 | // modified from https://github.com/revoltchat/frontend/blob/master/components/app/menus/ContextMenu.tsx 2 | // changed some styling 3 | 4 | import Icon, { IconProps } from "@components/Icon"; 5 | import { ComponentProps } from "react"; 6 | import styled from "styled-components"; 7 | 8 | export const ContextMenu = styled.div` 9 | display: flex; 10 | flex-direction: column; 11 | padding: 6px 8px; 12 | min-width: 200px; 13 | max-width: 300px; 14 | 15 | overflow: hidden; 16 | border-radius: 4px; 17 | background: var(--background-tertiary); 18 | color: var(--text); 19 | 20 | box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.5); 21 | `; 22 | 23 | export const ContextMenuDivider = styled.div` 24 | height: 1px; 25 | margin: 4px; 26 | background: var(--text-disabled); 27 | `; 28 | 29 | export const ContextMenuItem = styled("button")` 30 | display: block; 31 | padding: 6px 8px; 32 | border-radius: 4px; 33 | font-size: 14px; 34 | margin: 2px 0; 35 | cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; 36 | opacity: ${(props) => (props.disabled ? 0.5 : 1)}; 37 | 38 | // remove default button styles 39 | border: none; 40 | background: none; 41 | color: inherit; 42 | outline: none; 43 | `; 44 | 45 | const ButtonBase = styled(ContextMenuItem)<{ destructive?: boolean }>` 46 | display: flex; 47 | align-items: center; 48 | justify-content: space-between; 49 | gap: 8px; 50 | 51 | > span { 52 | margin-top: 1px; 53 | } 54 | 55 | &:hover { 56 | background: ${(props) => (props.destructive ? "var(--danger)" : "var(--primary)")}; 57 | ${(props) => (props.destructive ? `color: var(--text)` : "")} 58 | } 59 | 60 | ${(props) => (props.destructive ? `fill: var(--danger); color: var(--danger)` : "")} 61 | `; 62 | 63 | type ButtonProps = ComponentProps & { 64 | icon?: IconProps["icon"]; 65 | iconProps?: Omit; 66 | destructive?: boolean; 67 | }; 68 | 69 | export function ContextMenuButton({ icon, children, iconProps, ...props }: ButtonProps) { 70 | return ( 71 | 72 | {children} 73 | {icon && } 74 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /src/components/messaging/attachments/AttachmentUploadProgress.tsx: -------------------------------------------------------------------------------- 1 | import Icon from "@components/Icon"; 2 | import IconButton from "@components/IconButton"; 3 | import { useAppStore } from "@hooks/useAppStore"; 4 | import { QueuedMessage } from "@structures"; 5 | import { bytesToSize } from "@utils"; 6 | import { observer } from "mobx-react-lite"; 7 | import styled from "styled-components"; 8 | 9 | const Container = styled.div` 10 | width: 520px; 11 | min-width: auto; 12 | border: 1px solid transparent; 13 | padding: 10px; 14 | border-radius: 4px; 15 | background-color: var(--background-secondary); 16 | border-color: var(--background-secondary-alt); 17 | flex-direction: row; 18 | align-items: center; 19 | display: flex; 20 | `; 21 | 22 | const Wrapper = styled.div` 23 | flex: 1; 24 | white-space: nowrap; 25 | overflow: hidden; 26 | 27 | .muted { 28 | color: var(--text-secondary); 29 | } 30 | `; 31 | 32 | const Progress = styled.progress` 33 | height: 6px; 34 | width: 100%; 35 | `; 36 | 37 | const CustomIcon = styled(Icon)` 38 | color: var(--text-secondary); 39 | 40 | &:hover { 41 | color: var(--text); 42 | } 43 | `; 44 | 45 | interface Props { 46 | message: QueuedMessage; 47 | } 48 | 49 | function AttachmentUploadProgress({ message }: Props) { 50 | const app = useAppStore(); 51 | const totalSize = message.files!.reduce((p, f) => p + f.size, 0); 52 | 53 | return ( 54 | 55 | 56 |
62 | Uploading 63 | 64 | {message.files!.length === 1 ? message.files![0].name : `${message.files!.length} files`} 65 | 66 | - 67 | {bytesToSize(totalSize)} 68 |
69 | 70 |
71 | { 74 | message.abort(); 75 | // remove the message from the queue 76 | app.queue.remove(message.id); 77 | }} 78 | > 79 | 80 | 81 |
82 | ); 83 | } 84 | 85 | export default observer(AttachmentUploadProgress); 86 | -------------------------------------------------------------------------------- /src/components/contextMenus/GuildContextMenu.tsx: -------------------------------------------------------------------------------- 1 | // loosely based on https://github.com/revoltchat/frontend/blob/master/components/app/menus/UserContextMenu.tsx 2 | 3 | import { modalController } from "@/controllers/modals"; 4 | import { useAppStore } from "@hooks/useAppStore"; 5 | import useLogger from "@hooks/useLogger"; 6 | import { ChannelType } from "@spacebarchat/spacebar-api-types/v9"; 7 | import { Guild } from "@structures"; 8 | import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "./ContextMenu"; 9 | 10 | interface MenuProps { 11 | guild: Guild; 12 | } 13 | 14 | function GuildContextMenu({ guild }: MenuProps) { 15 | const app = useAppStore(); 16 | const logger = useLogger("GuildContextMenu"); 17 | const isNotOwner = guild.ownerId !== app.account!.id; 18 | /** 19 | * Copy id to clipboard 20 | */ 21 | function copyId() { 22 | navigator.clipboard.writeText(guild.id); 23 | } 24 | 25 | /** 26 | * Leave guild 27 | */ 28 | function leaveGuild() { 29 | modalController.push({ 30 | type: "leave_server", 31 | target: guild, 32 | }); 33 | } 34 | 35 | /** 36 | * Open invite creation modal 37 | */ 38 | function openInviteCreateModal() { 39 | const channel = guild.channels.find((x) => x.type === ChannelType.GuildText && x.hasPermission("VIEW_CHANNEL")); 40 | if (!channel) { 41 | logger.error("Failed to find suitable channel for invite creation"); 42 | return; 43 | } 44 | modalController.push({ 45 | type: "create_invite", 46 | target: channel, 47 | }); 48 | } 49 | 50 | return ( 51 | 52 | Create Invite 53 | 54 | {isNotOwner && ( 55 | <> 56 | 57 | Leave Guild 58 | 59 | 60 | 61 | )} 62 | 73 | Copy Guild ID 74 | 75 | 76 | ); 77 | } 78 | 79 | export default GuildContextMenu; 80 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export interface RouteSettings { 2 | api: string; 3 | cdn: string; 4 | gateway: string; 5 | wellknown: string; 6 | } 7 | 8 | export const DefaultRouteSettings: RouteSettings = { 9 | api: "https://api.old.server.spacebar.chat/api", 10 | cdn: "https://cdn.old.server.spacebar.chat", 11 | gateway: "wss://gateway.old.server.spacebar.chat", 12 | wellknown: "https://spacebar.chat", 13 | }; 14 | 15 | // TODO: we should probably make our own 16 | export const USER_JOIN_MESSAGES = [ 17 | "{author} joined the party.", 18 | "{author} is here.", 19 | "Welcome, {author}. We hope you brought pizza.", 20 | "A wild {author} appeared.", 21 | "{author} just landed.", 22 | "{author} just slid into the server.", 23 | "{author} just showed up!", 24 | "Welcome {author}. Say hi!", 25 | "{author} hopped into the server.", 26 | "Everyone welcome {author}!", 27 | "Glad you're here, {author}.", 28 | "Good to see you, {author}.", 29 | "Yay you made it, {author}!", 30 | ]; 31 | 32 | // TODO: this should come from the server 33 | export const MAX_UPLOAD_SIZE = 1024 * 1024 * 1024; // 1GB, taken from spacebar server default 34 | export const EMBEDDABLE_VIDEO_MIMES = ["webm", "ogg", "mp4"]; // list of the mimetypes that can be used in a video element 35 | export const EMBEDDABLE_AUDIO_MIMES = ["mp3", "wav", "ogg", "x-wav", "mpeg", "mp4"]; // list of the mimetypes that can be used in an audio element 36 | export const EMBEDDABLE_IMAGE_MIMES = ["png", "jpg", "jpeg", "gif", "webp"]; // list of mimetypes that can be used in an image element 37 | export const EMBEDDABLE_TEXT_MIMES = ["plain", "txt", "css", "csv", "html", "js"]; // list of mimetypes that can be used in a text element 38 | export const ARCHIVE_MIMES = ["zip", "tar", "tar.gz", "tar.xz", "tar.bz2", "rar", "7z"]; // list of mimetypes to associate with archives 39 | 40 | export const MAX_ATTACHMENTS = 15; // max number of attachments per message 41 | 42 | // matches discord.gg/:code and discordapp.com/invite/:code 43 | export const DISCORD_INVITE_REGEX = 44 | /(?:^|\b)discord(?:(?:app)?\.com\/invite|\.gg(?:\/invite)?)\/(?[\w-]{2,255})(?:$|\b)/gi; 45 | // matches :host/invite/:code 46 | export const SPACEBAR_INVITE_REGEX = /(?:^|\b)[\w\\.-]+(?:\/invite|\.gg(?:\/invite)?)\/(?[\w-]{2,255})(?:$|\b)/gi; 47 | --------------------------------------------------------------------------------