├── .babelrc ├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .prettierrc.js ├── .watchmanconfig ├── App.tsx ├── LICENSE ├── README.md ├── __tests__ └── App-test.tsx ├── android ├── app │ ├── build.gradle │ ├── debug.keystore │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── site │ │ │ │ └── endl │ │ │ │ └── taiku │ │ │ │ └── rvmob │ │ │ │ └── ReactNativeFlipper.java │ │ └── res │ │ │ ├── drawable │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ └── ic_launcher_monochrome.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ └── strings.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── java │ │ │ └── site │ │ │ │ └── endl │ │ │ │ └── taiku │ │ │ │ └── rvmob │ │ │ │ ├── MainActivity.java │ │ │ │ └── MainApplication.java │ │ └── res │ │ │ ├── drawable │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_launcher_foreground.xml │ │ │ └── ic_launcher_monochrome.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── release │ │ └── site │ │ └── endl │ │ └── taiku │ │ └── rvmob │ │ └── ReactNativeFlipper.java ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── app.json ├── assets ├── fonts │ ├── Inter │ │ ├── Inter.ttf │ │ ├── Inter_Black.ttf │ │ ├── Inter_Bold.ttf │ │ ├── Inter_ExtraBold.ttf │ │ ├── Inter_ExtraLight.ttf │ │ ├── Inter_Light.ttf │ │ ├── Inter_Medium.ttf │ │ ├── Inter_SemiBold.ttf │ │ └── Inter_Thin.ttf │ └── Open Sans │ │ ├── Open Sans.ttf │ │ ├── Open Sans_bold.ttf │ │ ├── Open Sans_bold_italic.ttf │ │ └── Open Sans_italic.ttf └── images │ ├── icon_debug.png │ └── icon_release.png ├── babel.config.js ├── index.js ├── ios ├── Podfile ├── Revvy.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── Revvy.xcscheme ├── Revvy │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ ├── LaunchScreen.storyboard │ └── main.m ├── RevvyTests │ ├── Info.plist │ └── RevvyTests.m └── link-assets-manifest.json ├── metro.config.js ├── package.json ├── react-native.config.js ├── rvmob-full.png ├── screenshots ├── 1.png ├── 2.png ├── 3.png ├── 4.png └── 5.png ├── shim.js ├── src ├── Generic.tsx ├── MessageBox.tsx ├── MessageView.tsx ├── Modals.tsx ├── Profile.tsx ├── SideMenus.tsx ├── Theme.tsx ├── components │ ├── NetworkIndicator.tsx │ ├── Notification.tsx │ ├── common │ │ ├── MarkdownView.tsx │ │ ├── atoms │ │ │ ├── Button.tsx │ │ │ ├── Checkbox.tsx │ │ │ ├── ContextButton.tsx │ │ │ ├── CopyIDButton.tsx │ │ │ ├── Link.tsx │ │ │ ├── Text.tsx │ │ │ └── index.tsx │ │ └── messaging │ │ │ ├── InviteEmbed.tsx │ │ │ ├── Message.tsx │ │ │ ├── MessageEmbed.tsx │ │ │ ├── ReplyMessage.tsx │ │ │ └── index.ts │ ├── navigation │ │ ├── ChannelHeader.tsx │ │ ├── ChannelList.tsx │ │ ├── ServerList.tsx │ │ └── UserList.tsx │ ├── pages │ │ ├── FriendsPage.tsx │ │ ├── HomePage.tsx │ │ ├── LoginSettingsPage.tsx │ │ └── VoiceChannel.tsx │ ├── sheets │ │ ├── ChannelInfoSheet.tsx │ │ ├── MemberListSheet.tsx │ │ ├── MessageMenuSheet.tsx │ │ ├── ProfileSheet.tsx │ │ ├── ReportSheet.tsx │ │ ├── ServerInfoSheet.tsx │ │ ├── SettingsSheet.tsx │ │ ├── UserMenuSheet.tsx │ │ └── index.tsx │ └── views │ │ └── ChannelView.tsx └── lib │ ├── consts.ts │ └── utils.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "@babel/plugin-proposal-decorators", { "legacy": true } 5 | ] 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Windows files 2 | [*.bat] 3 | end_of_line = crlf 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native-community', 4 | parser: '@typescript-eslint/parser', 5 | plugins: ['@typescript-eslint'], 6 | rules: { 7 | 'react-native/no-inline-styles': 0, // TODO: turn this back on when the codebase is less error-filled 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Windows files should use crlf line endings 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | *.bat text eol=crlf 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build RVMob (non-release) 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | # build-win: 8 | # runs-on: windows-latest 9 | # steps: 10 | # - name: Checkout Repository 11 | # uses: actions/checkout@v3 12 | # - name: Set up Node 13 | # uses: actions/setup-node@v3 14 | # with: 15 | # node-version: 18 16 | # cache: "yarn" 17 | # - name: Set up Java 18 | # uses: actions/setup-java@v3 19 | # with: 20 | # java-version: '11' 21 | # distribution: 'microsoft' 22 | # - name: Install and prepare dependencies 23 | # run: yarn install && npx rn-nodeify -e 24 | # - name: Build the app 25 | # run: npx react-native build-android --mode debug 26 | # - name: Upload debug APK 27 | # uses: actions/upload-artifact@v3 28 | # with: 29 | # path: D:\a\rvmob\rvmob\android\app\build\outputs\apk\debug\app-debug.apk 30 | # name: RVMob-Debug-${{ github.sha }}.apk 31 | build-linux: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Checkout Repository 35 | uses: actions/checkout@v3 36 | - name: Set up Node 37 | uses: actions/setup-node@v3 38 | with: 39 | node-version: 18 40 | cache: 'yarn' 41 | - name: Set up Java 42 | uses: actions/setup-java@v3 43 | with: 44 | java-version: '11' 45 | distribution: 'microsoft' 46 | - name: Install and prepare dependencies 47 | run: yarn install && npx rn-nodeify -e && npx react-native-asset 48 | - name: Bundle the JS 49 | run: npx react-native bundle --platform android --entry-file ./index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res 50 | - name: Build the app 51 | run: npx react-native build-android --mode debug 52 | - name: Prepare debug APK 53 | run: mv app-debug.apk RVMob-Debug-${{ github.sha }}.apk 54 | working-directory: /home/runner/work/dvmob/dvmob/android/app/build/outputs/apk/debug/ 55 | - name: Upload debug APK 56 | uses: actions/upload-artifact@v3 57 | with: 58 | path: /home/runner/work/dvmob/dvmob/android/app/build/outputs/apk/debug/RVMob-Debug-${{ github.sha }}.apk 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # Keys 25 | *.keystore 26 | !debug.keystore 27 | 28 | # Android/IntelliJ 29 | # 30 | build/ 31 | .idea 32 | .gradle 33 | local.properties 34 | *.iml 35 | index.android.bundle 36 | .cxx 37 | link-assets-manifest.json 38 | android/app/src/main/assets/fonts 39 | 40 | # node.js 41 | # 42 | node_modules/ 43 | npm-debug.log 44 | yarn-error.log 45 | 46 | # fastlane 47 | # 48 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 49 | # screenshots whenever they are needed. 50 | # For more information about the recommended setup visit: 51 | # https://docs.fastlane.tools/best-practices/source-control/ 52 | 53 | */fastlane/report.xml 54 | */fastlane/Preview.html 55 | */fastlane/screenshots 56 | 57 | # Bundle artifact 58 | *.jsbundle 59 | 60 | # CocoaPods 61 | /ios/Pods/ 62 | 63 | # Temporary files created by Metro to check the health of the file watcher 64 | .metro-health-check* -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: false, 3 | jsxBracketSameLine: true, 4 | singleQuote: true, // TODO: switch this to false at an opportune moment 5 | trailingComma: 'all', 6 | arrowParens: 'avoid', 7 | endOfLine: 'auto', 8 | }; 9 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notice 2 | This fork has now been archived as RVMob now supports instances, simply connect with `https://api.divolt.xyz` and restart. Any issues with instance switching should be opened there. 3 | 4 | --- 5 | 6 | # DVMob 7 | 8 | **DVMob** is a mobile Divolt client made in React Native. 9 | 10 | DVMob is currently in beta, exclusive to Android and pretty unoptimized - use at your own discretion. 11 | 12 | ## Building 13 | 14 | Right now, if you want to install DVMob you will have to build it yourself. You'll need [Node](https://nodejs.org/en/), [Yarn Classic](https://classic.yarnpkg.com) and [npx](https://www.npmjs.com/package/npx). Then run the following: 15 | 16 | ```sh 17 | yarn install 18 | npx rn-nodeify -e 19 | yarn start 20 | ``` 21 | 22 | CLI commands: 23 | 24 | | Command | Description | 25 | | -------------- | -------------------------------------- | 26 | | `yarn start` | Starts the application. | 27 | | `yarn test` | Tests to see if everything is working. | 28 | | `yarn android` | Runs an Android version. | 29 | | `yarn ios` | Runs an iOS version (requires a Mac). | 30 | | `yarn lint` | Checks the code syntax using ESLint. | 31 | 32 | For more see a list of `react-native`'s commands [here](https://github.com/react-native-community/cli/blob/master/docs/commands.md). You can access them by running `npx react-native`. 33 | 34 | ## Troubleshooting 35 | 36 | If you encounter bugs, first check if you're able to [open Divolt in your browser](https://divolt.xyz). Also check if you have any firewall settings that may block the Revolt API. 37 | 38 | If you're still experiencing issues, and there aren't any open issues for the bug(s) you're facing, please [open an issue](https://github.com/fmhy/dvmob/issues). 39 | 40 | ## License 41 | 42 | DVMob is licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/rvmob/blob/master/LICENSE). 43 | -------------------------------------------------------------------------------- /__tests__/App-test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import 'react-native'; 6 | import React from 'react'; 7 | import App from '../App'; 8 | 9 | // Note: test renderer must be required after react-native. 10 | import renderer from 'react-test-renderer'; 11 | 12 | it('renders correctly', () => { 13 | renderer.create(); 14 | }); 15 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | apply plugin: "com.facebook.react" 3 | 4 | import com.android.build.OutputFile 5 | 6 | /** 7 | * This is the configuration block to customize your React Native Android app. 8 | * By default you don't need to apply any configuration, just uncomment the lines you need. 9 | */ 10 | react { 11 | /* Folders */ 12 | // The root of your project, i.e. where "package.json" lives. Default is '..' 13 | // root = file("../") 14 | // The folder where the react-native NPM package is. Default is ../node_modules/react-native 15 | // reactNativeDir = file("../node-modules/react-native") 16 | // The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen 17 | // codegenDir = file("../node-modules/react-native-codegen") 18 | // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js 19 | // cliFile = file("../node_modules/react-native/cli.js") 20 | /* Variants */ 21 | // The list of variants to that are debuggable. For those we're going to 22 | // skip the bundling of the JS bundle and the assets. By default is just 'debug'. 23 | // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. 24 | // debuggableVariants = ["liteDebug", "prodDebug"] 25 | /* Bundling */ 26 | // A list containing the node command and its flags. Default is just 'node'. 27 | // nodeExecutableAndArgs = ["node"] 28 | // 29 | // The command to run when bundling. By default is 'bundle' 30 | // bundleCommand = "ram-bundle" 31 | // 32 | // The path to the CLI configuration file. Default is empty. 33 | // bundleConfig = file(../rn-cli.config.js) 34 | // 35 | // The name of the generated asset file containing your JS bundle 36 | // bundleAssetName = "MyApplication.android.bundle" 37 | // 38 | // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' 39 | // entryFile = file("../js/MyApplication.android.js") 40 | // 41 | // A list of extra flags to pass to the 'bundle' commands. 42 | // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle 43 | // extraPackagerArgs = [] 44 | /* Hermes Commands */ 45 | // The hermes compiler command to run. By default it is 'hermesc' 46 | // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" 47 | // 48 | // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" 49 | // hermesFlags = ["-O", "-output-source-map"] 50 | } 51 | 52 | apply from: "../../node_modules/react-native-vector-icons/fonts.gradle" 53 | 54 | /** 55 | * Set this to true to create four separate APKs instead of one, 56 | * one for each native architecture. This is useful if you don't 57 | * use App Bundles (https://developer.android.com/guide/app-bundle/) 58 | * and want to have separate APKs to upload to the Play Store. 59 | */ 60 | def enableSeparateBuildPerCPUArchitecture = false 61 | 62 | /** 63 | * Set this to true to run Proguard on Release builds to minify the Java bytecode. 64 | */ 65 | def enableProguardInReleaseBuilds = false 66 | 67 | /** 68 | * The preferred build flavor of JavaScriptCore (JSC). 69 | * 70 | * For example, to use the international variant, you can use: 71 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` 72 | * 73 | * The international variant includes ICU i18n library and necessary data 74 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that 75 | * give correct results when using with locales other than en-US. Note that 76 | * this variant is about 6MiB larger per architecture than default. 77 | */ 78 | def jscFlavor = 'org.webkit:android-jsc-intl:+' 79 | 80 | /** 81 | * Private function to get the list of Native Architectures you want to build. 82 | * This reads the value from reactNativeArchitectures in your gradle.properties 83 | * file and works together with the --active-arch-only flag of react-native run-android. 84 | */ 85 | def reactNativeArchitectures() { 86 | def value = project.getProperties().get("reactNativeArchitectures") 87 | return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] 88 | } 89 | 90 | android { 91 | ndkVersion rootProject.ext.ndkVersion 92 | 93 | compileSdkVersion rootProject.ext.compileSdkVersion 94 | 95 | namespace "site.endl.taiku.rvmob" 96 | defaultConfig { 97 | applicationId "site.endl.taiku.rvmob" 98 | minSdkVersion rootProject.ext.minSdkVersion 99 | targetSdkVersion rootProject.ext.targetSdkVersion 100 | versionCode 5 101 | versionName "0.4.0" 102 | } 103 | splits { 104 | abi { 105 | reset() 106 | enable enableSeparateBuildPerCPUArchitecture 107 | universalApk false // If true, also generate a universal APK 108 | include (*reactNativeArchitectures()) 109 | } 110 | } 111 | signingConfigs { 112 | debug { 113 | storeFile file('debug.keystore') 114 | storePassword 'android' 115 | keyAlias 'androiddebugkey' 116 | keyPassword 'android' 117 | } 118 | release { 119 | storeFile file('.keystore') 120 | storePassword '' 121 | keyAlias '' 122 | keyPassword '' 123 | } 124 | } 125 | buildTypes { 126 | debug { 127 | signingConfig signingConfigs.debug 128 | applicationIdSuffix '.debug' 129 | } 130 | release { 131 | // Caution! In production, you need to generate your own keystore file. 132 | // see https://reactnative.dev/docs/signed-apk-android. 133 | signingConfig signingConfigs.debug 134 | minifyEnabled enableProguardInReleaseBuilds 135 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 136 | } 137 | } 138 | 139 | // applicationVariants are e.g. debug, release 140 | applicationVariants.all { variant -> 141 | variant.outputs.each { output -> 142 | // For each separate APK per architecture, set a unique version code as described here: 143 | // https://developer.android.com/studio/build/configure-apk-splits.html 144 | // Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc. 145 | def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] 146 | def abi = output.getFilter(OutputFile.ABI) 147 | if (abi != null) { // null for the universal-debug, universal-release variants 148 | output.versionCodeOverride = 149 | defaultConfig.versionCode * 1000 + versionCodes.get(abi) 150 | } 151 | 152 | } 153 | } 154 | packagingOptions { 155 | // Make sure libjsc.so does not packed in APK 156 | exclude "**/libjsc.so" 157 | } 158 | } 159 | 160 | dependencies { 161 | // The version of react-native is set by the React Native Gradle Plugin 162 | implementation("com.facebook.react:react-android") 163 | 164 | implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0") 165 | 166 | // implementation('com.ts:auth-control-sdk:5.1.1@aar') { transitive=true } 167 | 168 | debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") 169 | debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { 170 | exclude group:'com.squareup.okhttp3', module:'okhttp' 171 | } 172 | 173 | debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") 174 | 175 | // if (hermesEnabled.toBoolean()) { 176 | implementation("com.facebook.react:hermes-android") 177 | // } else { 178 | // implementation jscFlavor 179 | // } 180 | } 181 | 182 | apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) 183 | -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/debug.keystore -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -keep class com.facebook.hermes.unicode.** { *; } 12 | -keep class com.facebook.jni.** { *; } 13 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /android/app/src/debug/java/site/endl/taiku/rvmob/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package site.endl.taiku.rvmob; 8 | 9 | import android.content.Context; 10 | import com.facebook.flipper.android.AndroidFlipperClient; 11 | import com.facebook.flipper.android.utils.FlipperUtils; 12 | import com.facebook.flipper.core.FlipperClient; 13 | import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; 14 | import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; 15 | import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; 16 | import com.facebook.flipper.plugins.inspector.DescriptorMapping; 17 | import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; 18 | import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; 19 | import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; 20 | import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; 21 | import com.facebook.react.ReactInstanceManager; 22 | import com.facebook.react.bridge.ReactContext; 23 | import com.facebook.react.modules.network.NetworkingModule; 24 | import okhttp3.OkHttpClient; 25 | 26 | public class ReactNativeFlipper { 27 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 28 | if (FlipperUtils.shouldEnableFlipper(context)) { 29 | final FlipperClient client = AndroidFlipperClient.getInstance(context); 30 | 31 | client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); 32 | client.addPlugin(new DatabasesFlipperPlugin(context)); 33 | client.addPlugin(new SharedPreferencesFlipperPlugin(context)); 34 | client.addPlugin(CrashReporterPlugin.getInstance()); 35 | 36 | NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); 37 | NetworkingModule.setCustomClientBuilder( 38 | new NetworkingModule.CustomClientBuilder() { 39 | @Override 40 | public void apply(OkHttpClient.Builder builder) { 41 | builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); 42 | } 43 | }); 44 | client.addPlugin(networkFlipperPlugin); 45 | client.start(); 46 | 47 | // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized 48 | // Hence we run if after all native modules have been initialized 49 | ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); 50 | if (reactContext == null) { 51 | reactInstanceManager.addReactInstanceEventListener( 52 | new ReactInstanceManager.ReactInstanceEventListener() { 53 | @Override 54 | public void onReactContextInitialized(ReactContext reactContext) { 55 | reactInstanceManager.removeReactInstanceEventListener(this); 56 | reactContext.runOnNativeModulesQueueThread( 57 | new Runnable() { 58 | @Override 59 | public void run() { 60 | client.addPlugin(new FrescoFlipperPlugin()); 61 | } 62 | }); 63 | } 64 | }); 65 | } else { 66 | client.addPlugin(new FrescoFlipperPlugin()); 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /android/app/src/debug/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /android/app/src/debug/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /android/app/src/debug/res/drawable/ic_launcher_monochrome.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/debug/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/debug/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/debug/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/debug/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | RVMob Canary 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 14 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /android/app/src/main/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/main/assets/.gitkeep -------------------------------------------------------------------------------- /android/app/src/main/java/site/endl/taiku/rvmob/MainActivity.java: -------------------------------------------------------------------------------- 1 | package site.endl.taiku.rvmob; 2 | 3 | import com.facebook.react.ReactActivity; 4 | 5 | public class MainActivity extends ReactActivity { 6 | 7 | /** 8 | * Returns the name of the main component registered from JavaScript. This is used to schedule 9 | * rendering of the component. 10 | */ 11 | @Override 12 | protected String getMainComponentName() { 13 | return "Revvy"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /android/app/src/main/java/site/endl/taiku/rvmob/MainApplication.java: -------------------------------------------------------------------------------- 1 | package site.endl.taiku.rvmob; 2 | 3 | import android.app.Application; 4 | import com.facebook.react.PackageList; 5 | import com.facebook.react.ReactApplication; 6 | import io.invertase.notifee.NotifeePackage; 7 | import com.reactnativecommunity.asyncstorage.AsyncStoragePackage; 8 | import org.linusu.RNGetRandomValuesPackage; 9 | import com.bitgo.randombytes.RandomBytesPackage; 10 | import com.facebook.react.ReactNativeHost; 11 | import com.facebook.react.modules.systeminfo.AndroidInfoHelpers; 12 | import com.facebook.react.ReactPackage; 13 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; 14 | import com.facebook.react.defaults.DefaultReactNativeHost; 15 | import com.facebook.soloader.SoLoader; 16 | import java.util.List; 17 | 18 | public class MainApplication extends Application implements ReactApplication { 19 | 20 | private final ReactNativeHost mReactNativeHost = 21 | new DefaultReactNativeHost(this) { 22 | @Override 23 | public boolean getUseDeveloperSupport() { 24 | return BuildConfig.DEBUG; 25 | } 26 | 27 | @Override 28 | protected List getPackages() { 29 | @SuppressWarnings("UnnecessaryLocalVariable") 30 | List packages = new PackageList(this).getPackages(); 31 | // Packages that cannot be autolinked yet can be added manually here, for example: 32 | // packages.add(new MyReactNativePackage()); 33 | return packages; 34 | } 35 | 36 | @Override 37 | protected String getJSMainModuleName() { 38 | return "index"; 39 | } 40 | 41 | @Override 42 | protected boolean isNewArchEnabled() { 43 | return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; 44 | } 45 | 46 | @Override 47 | protected Boolean isHermesEnabled() { 48 | return BuildConfig.IS_HERMES_ENABLED; 49 | } 50 | }; 51 | 52 | @Override 53 | public ReactNativeHost getReactNativeHost() { 54 | return mReactNativeHost; 55 | } 56 | 57 | @Override 58 | public void onCreate() { 59 | super.onCreate(); 60 | SoLoader.init(this, /* native exopackage */ false); 61 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 62 | // If you opted-in for the New Architecture, we load the native entry point for this app. 63 | DefaultNewArchitectureEntryPoint.load(); 64 | } 65 | ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_launcher_monochrome.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | RVMob 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /android/app/src/release/site/endl/taiku/rvmob/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.rndiffapp; 8 | import android.content.Context; 9 | import com.facebook.react.ReactInstanceManager; 10 | /** 11 | * Class responsible of loading Flipper inside your React Native application. This is the release 12 | * flavor of it so it's empty as we don't want to load Flipper. 13 | */ 14 | public class ReactNativeFlipper { 15 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 16 | // Do nothing as we don't want to initialize Flipper on Release. 17 | } 18 | } -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext { 4 | buildToolsVersion = "33.0.0" 5 | minSdkVersion = 21 6 | compileSdkVersion = 33 7 | targetSdkVersion = 33 8 | // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. 9 | ndkVersion = "23.1.7779620" 10 | } 11 | repositories { 12 | google() 13 | mavenCentral() 14 | } 15 | dependencies { 16 | classpath('com.android.tools.build:gradle:7.4.1') 17 | classpath("com.facebook.react:react-native-gradle-plugin") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 15 | 16 | # When configured, Gradle will run in incubating parallel mode. 17 | # This option should only be used with decoupled projects. More details, visit 18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 19 | # org.gradle.parallel=true 20 | 21 | # AndroidX package structure to make it clearer which packages are bundled with the 22 | # Android operating system, and which are packaged with your app's APK 23 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 24 | android.useAndroidX=true 25 | # Automatically convert third-party libraries to use AndroidX 26 | android.enableJetifier=true 27 | 28 | # Version of flipper SDK to use with React Native 29 | FLIPPER_VERSION=0.93.0 30 | 31 | # Use this property to enable or disable the Hermes JS engine. 32 | # If set to false, you will be using JSC instead. 33 | hermesEnabled=true 34 | 35 | newArchEnabled=true -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'Revvy' 2 | includeBuild('../node_modules/react-native-gradle-plugin') 3 | include ':@notifee_react-native' 4 | project(':@notifee_react-native').projectDir = new File(rootProject.projectDir, '../node_modules/@notifee/react-native/android') 5 | include ':@react-native-async-storage_async-storage' 6 | project(':@react-native-async-storage_async-storage').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-async-storage/async-storage/android') 7 | include ':react-native-get-random-values' 8 | project(':react-native-get-random-values').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-get-random-values/android') 9 | include ':react-native-randombytes' 10 | project(':react-native-randombytes').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-randombytes/android') 11 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 12 | include ':app' -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Revvy", 3 | "displayName": "DVMob" 4 | } 5 | -------------------------------------------------------------------------------- /assets/fonts/Inter/Inter.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/assets/fonts/Inter/Inter.ttf -------------------------------------------------------------------------------- /assets/fonts/Inter/Inter_Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/assets/fonts/Inter/Inter_Black.ttf -------------------------------------------------------------------------------- /assets/fonts/Inter/Inter_Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/assets/fonts/Inter/Inter_Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/Inter/Inter_ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/assets/fonts/Inter/Inter_ExtraBold.ttf -------------------------------------------------------------------------------- /assets/fonts/Inter/Inter_ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/assets/fonts/Inter/Inter_ExtraLight.ttf -------------------------------------------------------------------------------- /assets/fonts/Inter/Inter_Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/assets/fonts/Inter/Inter_Light.ttf -------------------------------------------------------------------------------- /assets/fonts/Inter/Inter_Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/assets/fonts/Inter/Inter_Medium.ttf -------------------------------------------------------------------------------- /assets/fonts/Inter/Inter_SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/assets/fonts/Inter/Inter_SemiBold.ttf -------------------------------------------------------------------------------- /assets/fonts/Inter/Inter_Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/assets/fonts/Inter/Inter_Thin.ttf -------------------------------------------------------------------------------- /assets/fonts/Open Sans/Open Sans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/assets/fonts/Open Sans/Open Sans.ttf -------------------------------------------------------------------------------- /assets/fonts/Open Sans/Open Sans_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/assets/fonts/Open Sans/Open Sans_bold.ttf -------------------------------------------------------------------------------- /assets/fonts/Open Sans/Open Sans_bold_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/assets/fonts/Open Sans/Open Sans_bold_italic.ttf -------------------------------------------------------------------------------- /assets/fonts/Open Sans/Open Sans_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/assets/fonts/Open Sans/Open Sans_italic.ttf -------------------------------------------------------------------------------- /assets/images/icon_debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/assets/images/icon_debug.png -------------------------------------------------------------------------------- /assets/images/icon_release.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/assets/images/icon_release.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import {AppRegistry} from 'react-native'; 6 | import {App} from './App'; 7 | import {name as appName} from './app.json'; 8 | 9 | AppRegistry.registerComponent(appName, () => App); 10 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | require_relative '../node_modules/react-native/scripts/react_native_pods' 2 | require_relative '../node_modules/react-native/scripts/native_modules' 3 | platform :ios, min_ios_version_supported 4 | prepare_react_native_project! 5 | flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled 6 | linkage = ENV['USE_FRAMEWORKS'] 7 | if linkage != nil 8 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green 9 | use_frameworks! :linkage => linkage.to_sym 10 | end 11 | 12 | target 'Revvy' do 13 | config = use_native_modules! 14 | 15 | use_react_native!( 16 | :path => config[:reactNativePath], 17 | # to enable hermes on iOS, change `false` to `true` and then install pods 18 | :hermes_enabled => false 19 | ) 20 | 21 | pod 'react-native-randombytes', :path => '../node_modules/react-native-randombytes' 22 | 23 | pod 'react-native-get-random-values', :path => '../node_modules/react-native-get-random-values' 24 | 25 | pod 'RNCAsyncStorage', :path => '../node_modules/@react-native-async-storage/async-storage' 26 | 27 | pod 'RNNotifee', :path => '../node_modules/@notifee/react-native' 28 | 29 | target 'RevvyTests' do 30 | inherit! :complete 31 | # Pods for testing 32 | end 33 | 34 | # Enables Flipper. 35 | # 36 | # Note that if you have use_frameworks! enabled, Flipper will not work and 37 | # you should disable the next line. 38 | use_flipper!() 39 | 40 | post_install do |installer| 41 | react_native_post_install(installer) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /ios/Revvy.xcodeproj/xcshareddata/xcschemes/Revvy.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /ios/Revvy/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | @interface AppDelegate : RCTAppDelegate 4 | @end -------------------------------------------------------------------------------- /ios/Revvy/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | #import 6 | 7 | #ifdef FB_SONARKIT_ENABLED 8 | #import 9 | #import 10 | #import 11 | #import 12 | #import 13 | #import 14 | 15 | static void InitializeFlipper(UIApplication *application) { 16 | FlipperClient *client = [FlipperClient sharedClient]; 17 | SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults]; 18 | [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]]; 19 | [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]]; 20 | [client addPlugin:[FlipperKitReactPlugin new]]; 21 | [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]]; 22 | [client start]; 23 | } 24 | #endif 25 | 26 | @implementation AppDelegate 27 | 28 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 29 | { 30 | #ifdef FB_SONARKIT_ENABLED 31 | InitializeFlipper(application); 32 | #endif 33 | 34 | RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; 35 | RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge 36 | moduleName:@"Revvy" 37 | initialProperties:nil]; 38 | 39 | if (@available(iOS 13.0, *)) { 40 | rootView.backgroundColor = [UIColor systemBackgroundColor]; 41 | } else { 42 | rootView.backgroundColor = [UIColor whiteColor]; 43 | } 44 | 45 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 46 | UIViewController *rootViewController = [UIViewController new]; 47 | rootViewController.view = rootView; 48 | self.window.rootViewController = rootViewController; 49 | [self.window makeKeyAndVisible]; 50 | return YES; 51 | } 52 | 53 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 54 | { 55 | #if DEBUG 56 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; 57 | #else 58 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 59 | #endif 60 | } 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /ios/Revvy/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /ios/Revvy/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ios/Revvy/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Revvy 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSExceptionDomains 30 | 31 | localhost 32 | 33 | NSExceptionAllowsInsecureHTTPLoads 34 | 35 | 36 | 37 | 38 | NSLocationWhenInUseUsageDescription 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UIViewControllerBasedStatusBarAppearance 53 | 54 | UIAppFonts 55 | 56 | AntDesign.ttf 57 | Entypo.ttf 58 | EvilIcons.ttf 59 | Feather.ttf 60 | FontAwesome.ttf 61 | FontAwesome5_Brands.ttf 62 | FontAwesome5_Regular.ttf 63 | FontAwesome5_Solid.ttf 64 | Fontisto.ttf 65 | Foundation.ttf 66 | Ionicons.ttf 67 | MaterialCommunityIcons.ttf 68 | MaterialIcons.ttf 69 | Octicons.ttf 70 | SimpleLineIcons.ttf 71 | Zocial.ttf 72 | Inter.ttf 73 | Inter_Black.ttf 74 | Inter_Bold.ttf 75 | Inter_ExtraBold.ttf 76 | Inter_ExtraLight.ttf 77 | Inter_Light.ttf 78 | Inter_Medium.ttf 79 | Inter_SemiBold.ttf 80 | Inter_Thin.ttf 81 | Open Sans.ttf 82 | Open Sans_bold.ttf 83 | Open Sans_bold_italic.ttf 84 | Open Sans_italic.ttf 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /ios/Revvy/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /ios/Revvy/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ios/RevvyTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ios/RevvyTests/RevvyTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #define TIMEOUT_SECONDS 600 8 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 9 | 10 | @interface RevvyTests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation RevvyTests 15 | 16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 17 | { 18 | if (test(view)) { 19 | return YES; 20 | } 21 | for (UIView *subview in [view subviews]) { 22 | if ([self findSubviewInView:subview matching:test]) { 23 | return YES; 24 | } 25 | } 26 | return NO; 27 | } 28 | 29 | - (void)testRendersWelcomeScreen 30 | { 31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 33 | BOOL foundElement = NO; 34 | 35 | __block NSString *redboxError = nil; 36 | #ifdef DEBUG 37 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 38 | if (level >= RCTLogLevelError) { 39 | redboxError = message; 40 | } 41 | }); 42 | #endif 43 | 44 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 45 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 46 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 47 | 48 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 49 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 50 | return YES; 51 | } 52 | return NO; 53 | }]; 54 | } 55 | 56 | #ifdef DEBUG 57 | RCTSetLogFunction(RCTDefaultLogFunction); 58 | #endif 59 | 60 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 61 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 62 | } 63 | 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /ios/link-assets-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "migIndex": 1, 3 | "data": [ 4 | { 5 | "path": "assets/fonts/Inter/Inter.ttf", 6 | "sha1": "7237d9cf55f177702066a28a4dde1e4c7e8ab576" 7 | }, 8 | { 9 | "path": "assets/fonts/Inter/Inter_Black.ttf", 10 | "sha1": "283fa65b96dc1f2d71c6504afb9610447bdcdf1b" 11 | }, 12 | { 13 | "path": "assets/fonts/Inter/Inter_Bold.ttf", 14 | "sha1": "48747b7a60086f97af0d373febcbd1f1bee87f17" 15 | }, 16 | { 17 | "path": "assets/fonts/Inter/Inter_ExtraBold.ttf", 18 | "sha1": "bff772be6bca9895c6fdcd57e5d5be9a47f2e058" 19 | }, 20 | { 21 | "path": "assets/fonts/Inter/Inter_ExtraLight.ttf", 22 | "sha1": "421072aef8610cb0f79661b494442a4b9363b43f" 23 | }, 24 | { 25 | "path": "assets/fonts/Inter/Inter_Light.ttf", 26 | "sha1": "dd9ec1ae67dd85143bacf7a3cb37e9f0ac979843" 27 | }, 28 | { 29 | "path": "assets/fonts/Inter/Inter_Medium.ttf", 30 | "sha1": "a418a8ba73bbcfa8c131c426ab836d78457afa9b" 31 | }, 32 | { 33 | "path": "assets/fonts/Inter/Inter_SemiBold.ttf", 34 | "sha1": "431007da316de60d85174aeec9b8389b5c73e7d6" 35 | }, 36 | { 37 | "path": "assets/fonts/Inter/Inter_Thin.ttf", 38 | "sha1": "297919d59c9ba4fa17251cdb9f06c26ca906e593" 39 | }, 40 | { 41 | "path": "assets/fonts/Open Sans/Open Sans.ttf", 42 | "sha1": "babe8dce93a3e48b6c3c79720a0c048e88dd1fe7" 43 | }, 44 | { 45 | "path": "assets/fonts/Open Sans/Open Sans_bold.ttf", 46 | "sha1": "1e3704ee48b5ff7e582488ead87b05249f14dc1c" 47 | }, 48 | { 49 | "path": "assets/fonts/Open Sans/Open Sans_bold_italic.ttf", 50 | "sha1": "72068cc905085ca406e6391cc5021ac043689ade" 51 | }, 52 | { 53 | "path": "assets/fonts/Open Sans/Open Sans_italic.ttf", 54 | "sha1": "b8d9f5ceb1e6b3a716dab0bdd09061bbc349b41e" 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Metro configuration for React Native 3 | * https://github.com/facebook/react-native 4 | * 5 | * @format 6 | */ 7 | 8 | module.exports = { 9 | transformer: { 10 | getTransformOptions: async () => ({ 11 | transform: { 12 | experimentalImportSupport: true, 13 | inlineRequires: true, 14 | }, 15 | }), 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rvmob", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "clean": "react-native-clean-project", 7 | "android": "react-native run-android", 8 | "ios": "react-native run-ios", 9 | "start": "react-native start", 10 | "test": "jest", 11 | "lint": "eslint .", 12 | "windows": "react-native run-windows" 13 | }, 14 | "dependencies": { 15 | "@babel/plugin-proposal-decorators": "^7.21.0", 16 | "@babel/preset-env": "^7.20.2", 17 | "@notifee/react-native": "^7.6.1", 18 | "@react-native-async-storage/async-storage": "^1.18.0", 19 | "@react-native-clipboard/clipboard": "^1.11.2", 20 | "@tradle/react-native-http": "^2.0.1", 21 | "@traptitech/markdown-it-spoiler": "^1.1.6", 22 | "@tsconfig/react-native": "^2.0.3", 23 | "@types/jest": "^29.5.0", 24 | "add": "^2.0.6", 25 | "assert": "^2.0.0", 26 | "browserify-zlib": "^0.2.0", 27 | "buffer": "^6.0.3", 28 | "console-browserify": "^1.2.0", 29 | "constants-browserify": "^1.0.0", 30 | "date-fns": "^2.25.0", 31 | "dayjs": "^1.10.7", 32 | "dns.js": "^1.0.1", 33 | "domain-browser": "^4.22.0", 34 | "events": "^3.3.0", 35 | "https-browserify": "^1.0.0", 36 | "markdown-it-regexp": "^0.4.0", 37 | "mobx-react": "^7.6.0", 38 | "mobx-react-lite": "^3.4.3", 39 | "path-browserify": "^1.0.1", 40 | "process": "^0.11.10", 41 | "punycode": "^2.3.0", 42 | "querystring-es3": "^0.2.1", 43 | "react": "^18.2.0", 44 | "react-dom": "^18.2.0", 45 | "react-error-boundary": "^4.0.3", 46 | "react-native": "^0.71.6", 47 | "react-native-bottom-drawer-view": "^1.0.3", 48 | "react-native-clean-project": "^4.0.1", 49 | "react-native-crypto": "^2.2.0", 50 | "react-native-device-info": "^10.6.0", 51 | "react-native-document-picker": "^8.2.0", 52 | "react-native-elements": "^3.4.3", 53 | "react-native-fast-image": "^8.6.3", 54 | "react-native-fs": "^2.18.0", 55 | "react-native-get-random-values": "^1.7.0", 56 | "react-native-image-viewing": "^0.2.0", 57 | "react-native-image-zoom-viewer": "^3.0.1", 58 | "react-native-level-fs": "^3.0.1", 59 | "react-native-markdown-display": "^7.0.0-alpha.2", 60 | "react-native-modal": "^13.0.1", 61 | "react-native-os": "https://github.com/aprock/react-native-os", 62 | "react-native-randombytes": "^3.6.1", 63 | "react-native-side-menu": "^1.1.3", 64 | "react-native-side-menu-updated": "^1.3.2", 65 | "react-native-sliding-modal": "^2.1.0", 66 | "react-native-tcp-socket": "^6.0.6", 67 | "react-native-udp": "^4.1.7", 68 | "react-native-vector-icons": "^9.0.0", 69 | "react-native-web": "^0.19.1", 70 | "react-native-windows": "^0.71.4", 71 | "readable-stream": "^4.3.0", 72 | "revolt.js": "^6.0.20", 73 | "stream-browserify": "^3.0.0", 74 | "timers-browserify": "^2.0.12", 75 | "tty-browserify": "^0.0.1", 76 | "url": "^0.11.0", 77 | "util": "^0.12.5", 78 | "vm-browserify": "^1.1.2", 79 | "yarn": "^1.22.19" 80 | }, 81 | "devDependencies": { 82 | "@babel/core": "^7.21.3", 83 | "@babel/runtime": "^7.21.0", 84 | "@react-native-community/eslint-config": "^3.2.0", 85 | "@types/assert": "^1.5.6", 86 | "@types/babel__core": "^7.20.0", 87 | "@types/eslint": "^8.21.3", 88 | "@types/events": "^3.0.0", 89 | "@types/punycode": "^2.1.0", 90 | "@types/react": "^18.0.33", 91 | "@types/react-dom": "^18.0.11", 92 | "@types/react-native-vector-icons": "^6.4.13", 93 | "@types/react-test-renderer": "^18.0.0", 94 | "@types/readable-stream": "^2.3.15", 95 | "babel-jest": "^29.5.0", 96 | "eslint": "^8.38.0", 97 | "jest": "^29.5.0", 98 | "metro-react-native-babel-preset": "^0.76.1", 99 | "react-native-codegen": "^0.72.0", 100 | "react-test-renderer": "^18.2.0", 101 | "rn-nodeify": "^10.3.0", 102 | "typescript": "^5.0.2" 103 | }, 104 | "jest": { 105 | "preset": "react-native" 106 | }, 107 | "react-native": { 108 | "zlib": "browserify-zlib", 109 | "console": "console-browserify", 110 | "constants": "constants-browserify", 111 | "crypto": "react-native-crypto", 112 | "dns": "dns.js", 113 | "net": "react-native-tcp", 114 | "domain": "domain-browser", 115 | "http": "@tradle/react-native-http", 116 | "https": "https-browserify", 117 | "os": "react-native-os", 118 | "path": "path-browserify", 119 | "querystring": "querystring-es3", 120 | "fs": "react-native-level-fs", 121 | "_stream_transform": "readable-stream/transform", 122 | "_stream_readable": "readable-stream/readable", 123 | "_stream_writable": "readable-stream/writable", 124 | "_stream_duplex": "readable-stream/duplex", 125 | "_stream_passthrough": "readable-stream/passthrough", 126 | "dgram": "react-native-udp", 127 | "stream": "stream-browserify", 128 | "timers": "timers-browserify", 129 | "tty": "tty-browserify", 130 | "vm": "vm-browserify", 131 | "tls": false 132 | }, 133 | "browser": { 134 | "zlib": "browserify-zlib", 135 | "console": "console-browserify", 136 | "constants": "constants-browserify", 137 | "crypto": "react-native-crypto", 138 | "dns": "dns.js", 139 | "net": "react-native-tcp", 140 | "domain": "domain-browser", 141 | "http": "@tradle/react-native-http", 142 | "https": "https-browserify", 143 | "os": "react-native-os", 144 | "path": "path-browserify", 145 | "querystring": "querystring-es3", 146 | "fs": "react-native-level-fs", 147 | "_stream_transform": "readable-stream/transform", 148 | "_stream_readable": "readable-stream/readable", 149 | "_stream_writable": "readable-stream/writable", 150 | "_stream_duplex": "readable-stream/duplex", 151 | "_stream_passthrough": "readable-stream/passthrough", 152 | "dgram": "react-native-udp", 153 | "stream": "stream-browserify", 154 | "timers": "timers-browserify", 155 | "tty": "tty-browserify", 156 | "vm": "vm-browserify", 157 | "tls": false 158 | }, 159 | "resolutions": { 160 | "revolt-api": "^0.5.17" 161 | }, 162 | "packageManager": "yarn@1.22.19" 163 | } 164 | -------------------------------------------------------------------------------- /react-native.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | project: { 3 | ios: {}, 4 | android: {} 5 | }, 6 | assets: ['./assets/fonts'] 7 | } -------------------------------------------------------------------------------- /rvmob-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/rvmob-full.png -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/screenshots/1.png -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/screenshots/2.png -------------------------------------------------------------------------------- /screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/screenshots/3.png -------------------------------------------------------------------------------- /screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/screenshots/4.png -------------------------------------------------------------------------------- /screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fmhy/dvmob/97df5088f7b47f94af6ef9f0ffa6a78d807c6ef3/screenshots/5.png -------------------------------------------------------------------------------- /shim.js: -------------------------------------------------------------------------------- 1 | if (typeof __dirname === 'undefined') global.__dirname = '/' 2 | if (typeof __filename === 'undefined') global.__filename = '' 3 | if (typeof process === 'undefined') { 4 | global.process = require('process') 5 | } else { 6 | const bProcess = require('process') 7 | for (var p in bProcess) { 8 | if (!(p in process)) { 9 | process[p] = bProcess[p] 10 | } 11 | } 12 | } 13 | 14 | process.browser = false 15 | if (typeof Buffer === 'undefined') global.Buffer = require('buffer').Buffer 16 | 17 | // global.location = global.location || { port: 80 } 18 | const isDev = typeof __DEV__ === 'boolean' && __DEV__ 19 | process.env['NODE_ENV'] = isDev ? 'development' : 'production' 20 | if (typeof localStorage !== 'undefined') { 21 | localStorage.debug = isDev ? '*' : '' 22 | } 23 | 24 | // If using the crypto shim, uncomment the following line to ensure 25 | // crypto is loaded first, so it can populate global.crypto 26 | require('crypto') 27 | -------------------------------------------------------------------------------- /src/Profile.tsx: -------------------------------------------------------------------------------- 1 | import {observer} from 'mobx-react-lite'; 2 | import React from 'react'; 3 | import {client, app} from './Generic'; 4 | import {currentTheme, styles} from './Theme'; 5 | import {Pressable, View} from 'react-native'; 6 | import FastImage from 'react-native-fast-image'; 7 | import {Server, User, Message, Channel} from 'revolt.js'; 8 | import {Text} from './components/common/atoms'; 9 | import {getColour} from './lib/utils'; 10 | import {DEFAULT_MAX_SIDE, USER_IDS} from './lib/consts'; 11 | 12 | const Image = FastImage; 13 | 14 | type UsernameProps = { 15 | server?: Server; 16 | user?: User; 17 | noBadge?: boolean; 18 | size?: number; 19 | masquerade?: string | null; 20 | color?: string; 21 | }; 22 | 23 | export const Username = observer( 24 | ({server, user, noBadge, size, masquerade, color}: UsernameProps) => { 25 | if (typeof user !== 'object') { 26 | return ( 27 | {''} 28 | ); 29 | } 30 | let memberObject = server 31 | ? client.members.getKey({ 32 | server: server?._id, 33 | user: user?._id, 34 | }) 35 | : undefined; 36 | let roleColor = color ? getColour(color) : styles.textDefault.color; 37 | let name = 38 | server && memberObject?.nickname 39 | ? memberObject?.nickname 40 | : user?.username; 41 | if (server && memberObject?.roles && memberObject?.roles?.length > 0) { 42 | let srv = client.servers.get(memberObject._id.server); 43 | if (srv?.roles) { 44 | for (let role of memberObject?.roles) { 45 | if (srv.roles[role].colour) { 46 | roleColor = getColour(srv.roles[role].colour!); 47 | } 48 | } 49 | } 50 | } 51 | let badgeSize = (size || 14) * 0.6; 52 | let bridgedMessage = 53 | user?._id === USER_IDS.automod && masquerade !== undefined; 54 | let badgeStyle = { 55 | color: currentTheme.accentColorForeground, 56 | backgroundColor: currentTheme.accentColor, 57 | marginLeft: badgeSize * 0.3, 58 | paddingLeft: badgeSize * 0.4, 59 | paddingRight: badgeSize * 0.4, 60 | borderRadius: 3, 61 | fontSize: badgeSize, 62 | height: badgeSize + badgeSize * 0.45, 63 | top: badgeSize * 0.5, 64 | }; 65 | return ( 66 | 67 | 70 | {masquerade ?? name} 71 | 72 | {!noBadge ? ( 73 | <> 74 | {bridgedMessage ? ( 75 | BRIDGE 76 | ) : ( 77 | <> 78 | {user?.bot ? BOT : null} 79 | {masquerade ? MASQ. : null} 80 | {user?._id === USER_IDS.platformModeration ? ( 81 | SYSTEM 82 | ) : null} 83 | 84 | )} 85 | 86 | ) : null} 87 | 88 | ); 89 | }, 90 | ); 91 | 92 | type AvatarProps = { 93 | channel?: Channel; 94 | user?: User; 95 | server?: Server; 96 | status?: boolean; 97 | size?: number; 98 | backgroundColor?: string; 99 | masquerade?: string; 100 | pressable?: boolean; 101 | }; 102 | 103 | export const Avatar = observer( 104 | ({ 105 | channel, 106 | user, 107 | server, 108 | status, 109 | size, 110 | backgroundColor, 111 | masquerade, 112 | pressable, 113 | }: AvatarProps) => { 114 | let memberObject = 115 | server && user 116 | ? client.members.getKey({ 117 | server: server?._id, 118 | user: user?._id, 119 | }) 120 | : null; 121 | let statusColor; 122 | let statusScale = 2.7; 123 | if (status) { 124 | const s = user?.online ? user.status?.presence || 'Online' : 'Offline'; 125 | statusColor = currentTheme[`status${s}`]; 126 | } 127 | let Container = pressable 128 | ? ({children}) => ( 129 | app.openImage(memberObject?.avatar || user?.avatar)}> 131 | {children} 132 | 133 | ) 134 | : View; 135 | if (user) { 136 | return ( 137 | 138 | 187 | ) : null} 188 | 189 | ); 190 | } 191 | if (channel) { 192 | return ( 193 | 194 | {channel?.generateIconURL() ? ( 195 | 227 | 234 | 237 | 243 | 249 | {user.online 250 | ? user.status?.text || user.status?.presence || 'Online' 251 | : 'Offline'} 252 | 253 | 254 | 255 | ); 256 | } 257 | 258 | if (channel) { 259 | return ( 260 | 261 | 262 | 263 | 269 | {channel.name} 270 | 271 | 277 | {channel?.recipient_ids?.length} members 278 | 279 | 280 | 281 | ); 282 | } 283 | 284 | return <>; 285 | }, 286 | ); 287 | 288 | type RoleViewProps = { 289 | server: Server; 290 | user: User; 291 | }; 292 | 293 | export const RoleView = observer(({server, user}: RoleViewProps) => { 294 | let memberObject = client.members.getKey({ 295 | server: server?._id, 296 | user: user?._id, 297 | }); 298 | 299 | let roles = memberObject?.roles?.map(r => server.roles![r]) || null; 300 | return memberObject && roles ? ( 301 | <> 302 | ROLES 303 | 306 | {roles.map(r => ( 307 | 318 | 329 | {r.name} 330 | 331 | ))} 332 | 333 | 334 | ) : null; 335 | }); 336 | -------------------------------------------------------------------------------- /src/SideMenus.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, TouchableOpacity, ScrollView, FlatList} from 'react-native'; 3 | 4 | import FAIcon from 'react-native-vector-icons/FontAwesome'; 5 | import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; 6 | 7 | import {Server, User} from 'revolt.js'; 8 | 9 | import {app, setFunction, client} from './Generic'; 10 | import {MiniProfile, Avatar} from './Profile'; 11 | import {styles, currentTheme} from './Theme'; 12 | import {Button, Text} from './components/common/atoms'; 13 | import {MarkdownView} from './components/common/MarkdownView'; 14 | import {ChannelList} from './components/navigation/ChannelList'; 15 | import {ServerList} from './components/navigation/ServerList'; 16 | import {DEFAULT_API_URL} from './lib/consts'; 17 | 18 | export const LeftMenu = ({ 19 | currentChannel, 20 | onChannelClick, 21 | onLogOut, 22 | orderedServers, 23 | }: { 24 | currentChannel: any; 25 | onChannelClick: Function; 26 | onLogOut: Function; 27 | orderedServers: string[]; 28 | }) => { 29 | const [currentServer, setCurrentServer] = React.useState( 30 | null as Server | null, 31 | ); 32 | setFunction('openServer', (s: Server | null) => { 33 | setCurrentServer(s); 34 | }); 35 | return ( 36 | <> 37 | 38 | 39 | { 41 | currentServer ? setCurrentServer(null) : app.openStatusMenu(true); 42 | }} 43 | onLongPress={() => { 44 | app.openProfile(client.user); 45 | }} 46 | delayLongPress={750} 47 | key={client.user?._id} 48 | style={{margin: 4}}> 49 | 56 | 57 | 65 | setCurrentServer(s)} 67 | onServerLongPress={(s: Server) => app.openServerContextMenu(s)} 68 | ordered={orderedServers} 69 | showDiscover={app.settings.get('app.instance') === DEFAULT_API_URL} 70 | /> 71 | 72 | 73 | 78 | 79 | 80 | 89 | 99 | 109 | 119 | 120 | 121 | ); 122 | }; 123 | -------------------------------------------------------------------------------- /src/Theme.tsx: -------------------------------------------------------------------------------- 1 | import {Dimensions, StyleSheet} from 'react-native'; 2 | export const themes = { 3 | Light: { 4 | background: '#F6F6F6', 5 | backgroundPrimary: '#FFFFFF', 6 | backgroundSecondary: '#F1F1F1', 7 | backgroundTertiary: '#4D4D4D', 8 | foregroundPrimary: '#000000', 9 | foregroundSecondary: '#F1F1F1', 10 | foregroundTertiary: '#4D4D4D', 11 | headerPrimary: '#F1F1F1', 12 | headerSecondary: '#F1F1F1', 13 | hover: '#0000002B', 14 | messageBox: '#F1F1F1', 15 | blockQuoteBackground: '#11111166', 16 | accentColor: '#1AD4B2', 17 | accentColorForeground: '#000000', 18 | contentType: 'light', 19 | statusOnline: '#3ABF7E', 20 | statusIdle: '#F39F00', 21 | statusBusy: '#F84848', 22 | statusFocus: '#4799F0', 23 | statusStreaming: '#977EFF', 24 | statusOffline: '#A5A5A5', 25 | statusInvisible: '#A5A5A5', 26 | error: '#ED4245', 27 | pingColor: '#FBFF0050', 28 | }, 29 | Dark: { 30 | background: '#191919', 31 | backgroundPrimary: '#242424', 32 | backgroundSecondary: '#1E1E1E', 33 | backgroundTertiary: '#4D4D4D', 34 | foregroundPrimary: '#F6F6F6', 35 | foregroundSecondary: '#C8C8C8', 36 | foregroundTertiary: '#848484', 37 | headerPrimary: '#363636', 38 | headerSecondary: '#2D2D2D', 39 | hover: '#0000001A', 40 | messageBox: '#363636', 41 | blockQuoteBackground: '#11111166', 42 | accentColor: '#1AD4B2', 43 | accentColorForeground: '#000000', 44 | contentType: 'light', 45 | statusOnline: '#3ABF7E', 46 | statusIdle: '#F39F00', 47 | statusBusy: '#F84848', 48 | statusFocus: '#4799F0', 49 | statusStreaming: '#977EFF', 50 | statusOffline: '#A5A5A5', 51 | statusInvisible: '#A5A5A5', 52 | error: '#ED4245', 53 | pingColor: '#FBFF000F', 54 | }, 55 | // "Solarized": { 56 | // backgroundPrimary: '#001a20', 57 | // backgroundSecondary: '#05252d', 58 | // blockQuoteBackground: '#11111166', 59 | // textPrimary: '#dddddd', 60 | // textSecondary: '#888888', 61 | // accentColor: '#1ad4b2', 62 | // accentColorForeground: '#000000', 63 | // contentType: 'light', 64 | // buttonBorderWidth: 0, 65 | // messageBoxBorderWidth: 0, 66 | // generalBorderWidth: 0, 67 | // buttonBorderColorActive: "#3333ff", 68 | // statusOnline: "#3abf7e", 69 | // statusIdle: "#f39f00", 70 | // statusBusy: "#f84848", 71 | // statusStreaming: "#977eff", 72 | // statusOffline: "#a5a5a5", 73 | // statusInvisible: "#a5a5a5", 74 | // pingColor: "#f84848", 75 | // pingColorForeground: "#ffffff" 76 | // }, 77 | // "Vibrant Pink": { 78 | // backgroundPrimary: '#f9bae9', 79 | // backgroundSecondary: '#e99cd6', 80 | // blockQuoteBackground: '#11111166', 81 | // textPrimary: '#000000', 82 | // textSecondary: '#555555', 83 | // accentColor: '#1ad4b2', 84 | // accentColorForeground: '#000000', 85 | // contentType: 'dark', 86 | // buttonBorderWidth: 0, 87 | // messageBoxBorderWidth: 0, 88 | // generalBorderWidth: 0, 89 | // buttonBorderColorActive: "#3333ff", 90 | // statusOnline: "#3abf7e", 91 | // statusIdle: "#f39f00", 92 | // statusBusy: "#f84848", 93 | // statusStreaming: "#977eff", 94 | // statusOffline: "#a5a5a5", 95 | // statusInvisible: "#a5a5a5", 96 | // pingColor: "#f84848", 97 | // pingColorForeground: "#ffffff" 98 | // }, 99 | // "AMOLED": { 100 | // backgroundPrimary: '#000000', 101 | // backgroundSecondary: '#000000', 102 | // foregroundPrimary: '#F6F6F6', 103 | // foregroundSecondary: '#C8C8C8', 104 | // foregroundTertiary: '#848484', 105 | // blockQuoteBackground: '#111111', 106 | // textPrimary: '#dddddd', 107 | // textSecondary: '#888888', 108 | // accentColor: '#1ad4b2', 109 | // accentColorForeground: '#000000', 110 | // contentType: 'light', 111 | // buttonBorderColor: "#ffffff99", 112 | // buttonBorderWidth: 1, 113 | // messageBoxBorderColor: "#ffffff99", 114 | // messageBoxBorderWidth: 1, 115 | // generalBorderColor: "#ffffff22", 116 | // generalBorderWidth: 1, 117 | // buttonBorderColorActive: "#3333ff", 118 | // statusOnline: "#3abf7e", 119 | // statusIdle: "#f39f00", 120 | // statusBusy: "#f84848", 121 | // statusStreaming: "#977eff", 122 | // statusOffline: "#a5a5a5", 123 | // statusInvisible: "#a5a5a5", 124 | // pingColor: "#f84848", 125 | // pingColorForeground: "#ffffff" 126 | // } 127 | }; 128 | export var currentTheme = themes['Dark']; 129 | export var currentThemeName = 'Dark'; 130 | 131 | export var styles: any; 132 | function refreshStyles() { 133 | styles = StyleSheet.create({ 134 | outer: { 135 | flex: 1, 136 | backgroundColor: currentTheme.backgroundSecondary, 137 | }, 138 | app: { 139 | flex: 1, 140 | backgroundColor: currentTheme.backgroundPrimary, 141 | }, 142 | mainView: { 143 | flex: 1, 144 | backgroundColor: currentTheme.backgroundPrimary, 145 | }, 146 | loginInput: { 147 | fontFamily: 'Inter', 148 | borderRadius: 8, 149 | padding: 3, 150 | paddingLeft: 10, 151 | paddingRight: 10, 152 | margin: 8, 153 | width: '80%', 154 | backgroundColor: currentTheme.backgroundSecondary, 155 | color: currentTheme.foregroundPrimary, 156 | }, 157 | loggingInScreen: { 158 | flex: 1, 159 | alignItems: 'center', 160 | justifyContent: 'center', 161 | }, 162 | flex: { 163 | flex: 1, 164 | }, 165 | header: { 166 | fontWeight: 'bold', 167 | fontSize: 16, 168 | }, 169 | headerv2: { 170 | fontWeight: 'bold', 171 | fontSize: 17.5, 172 | marginBottom: 10, 173 | }, 174 | profileSubheader: { 175 | fontWeight: 'bold', 176 | color: currentTheme.foregroundSecondary, 177 | marginVertical: 5, 178 | }, 179 | loadingHeader: { 180 | fontWeight: 'bold', 181 | textAlign: 'center', 182 | fontSize: 30, 183 | }, 184 | remark: { 185 | color: currentTheme.foregroundSecondary, 186 | textAlign: 'center', 187 | fontSize: 16, 188 | marginTop: 5, 189 | paddingHorizontal: 30, 190 | }, 191 | channelName: { 192 | flex: 1, 193 | fontWeight: 'bold', 194 | fontSize: 16, 195 | }, 196 | link: { 197 | color: currentTheme.accentColor, 198 | textDecorationLine: 'underline', 199 | // fontWeight: 'bold', 200 | }, 201 | leftView: { 202 | flex: 1, 203 | backgroundColor: currentTheme.backgroundSecondary, 204 | flexDirection: 'row', 205 | justifyContent: 'flex-start', 206 | }, 207 | rightView: { 208 | flex: 1, 209 | backgroundColor: currentTheme.backgroundSecondary, 210 | }, 211 | sheetBackground: { 212 | width: '100%', 213 | height: Dimensions.get('window').height * 0.75, 214 | top: '25%', 215 | padding: 15, 216 | backgroundColor: currentTheme.backgroundSecondary, 217 | }, 218 | textDefault: { 219 | color: currentTheme.foregroundPrimary, 220 | }, 221 | message: { 222 | width: '100%', 223 | flex: 1, 224 | flexDirection: 'row', 225 | }, 226 | messageGrouped: { 227 | paddingLeft: 35, 228 | width: '100%', 229 | }, 230 | messageInner: { 231 | flex: 1, 232 | paddingLeft: 10, 233 | }, 234 | messageAvatar: { 235 | width: 35, 236 | height: 35, 237 | borderRadius: 100000, 238 | }, 239 | messageAvatarReply: { 240 | width: 15, 241 | height: 15, 242 | borderRadius: 100000, 243 | }, 244 | messageUsernameReply: { 245 | marginHorizontal: 3, 246 | }, 247 | typingBar: { 248 | height: 26, 249 | paddingLeft: 6, 250 | padding: 3, 251 | backgroundColor: currentTheme.backgroundSecondary, 252 | borderBottomColor: currentTheme.backgroundPrimary, 253 | borderBottomWidth: 1, 254 | flexDirection: 'row', 255 | }, 256 | messageUsername: { 257 | fontWeight: 'bold', 258 | }, 259 | serverButton: { 260 | borderRadius: 5000, 261 | width: 48, 262 | height: 48, 263 | margin: 4, 264 | backgroundColor: currentTheme.backgroundPrimary, 265 | overflow: 'hidden', 266 | }, 267 | serverButtonInitials: { 268 | fontWeight: 'bold', 269 | textAlign: 'center', 270 | marginTop: '30%', 271 | }, 272 | serverIcon: { 273 | width: 48, 274 | height: 48, 275 | }, 276 | serverList: { 277 | width: 60, 278 | flexShrink: 1, 279 | backgroundColor: currentTheme.background, 280 | paddingVertical: 4, 281 | }, 282 | channelList: { 283 | flexGrow: 1000, 284 | flex: 1000, 285 | }, 286 | channelButton: { 287 | marginHorizontal: 8, 288 | borderRadius: 8, 289 | flexDirection: 'row', 290 | alignItems: 'center', 291 | }, 292 | button: { 293 | padding: 10, 294 | paddingHorizontal: 16, 295 | borderRadius: 8, 296 | backgroundColor: currentTheme.headerSecondary, 297 | margin: 5, 298 | justifyContent: 'center', 299 | alignItems: 'center', 300 | flexDirection: 'row', 301 | }, 302 | buttonSecondary: { 303 | padding: 10, 304 | paddingHorizontal: 16, 305 | borderRadius: 8, 306 | backgroundColor: currentTheme.backgroundPrimary, 307 | margin: 5, 308 | justifyContent: 'center', 309 | alignItems: 'center', 310 | flexDirection: 'row', 311 | }, 312 | channelButtonSelected: { 313 | backgroundColor: currentTheme.hover, 314 | }, 315 | iconContainer: { 316 | alignItems: 'center', 317 | justifyContent: 'center', 318 | width: 30, 319 | height: 30, 320 | marginRight: 8, 321 | }, 322 | messagesView: { 323 | padding: 10, 324 | flex: 1, 325 | }, 326 | messageBoxInner: { 327 | flexDirection: 'row', 328 | alignItems: 'center', 329 | minHeight: 50, 330 | paddingHorizontal: 8, 331 | }, 332 | messageBoxOuter: { 333 | backgroundColor: currentTheme.messageBox, 334 | overflow: 'hidden', 335 | }, 336 | sendButton: { 337 | margin: 3, 338 | justifyContent: 'center', 339 | alignItems: 'center', 340 | }, 341 | headerIcon: { 342 | margin: 5, 343 | marginRight: 10, 344 | justifyContent: 'center', 345 | alignItems: 'center', 346 | }, 347 | messageBox: { 348 | color: currentTheme.foregroundPrimary, 349 | paddingLeft: 10, 350 | padding: 6, 351 | flex: 1, 352 | fontFamily: 'Open Sans', 353 | }, 354 | serverName: { 355 | margin: 10, 356 | marginTop: 13, 357 | marginBottom: 9, 358 | fontSize: 18, 359 | fontWeight: 'bold', 360 | }, 361 | channelHeader: { 362 | height: 50, 363 | backgroundColor: currentTheme.headerPrimary, 364 | alignItems: 'center', 365 | paddingLeft: 20, 366 | flexDirection: 'row', 367 | }, 368 | messageContentReply: { 369 | height: 20, 370 | marginLeft: 4, 371 | }, 372 | actionTile: { 373 | height: 40, 374 | width: '100%', 375 | alignItems: 'center', 376 | flexDirection: 'row', 377 | backgroundColor: currentTheme.backgroundPrimary, 378 | borderRadius: 8, 379 | paddingLeft: 10, 380 | paddingRight: 10, 381 | marginTop: 5, 382 | }, 383 | messageBoxBar: { 384 | padding: 4, 385 | borderBottomColor: currentTheme.backgroundPrimary, 386 | borderBottomWidth: 1, 387 | flexDirection: 'row', 388 | }, 389 | attachmentsBar: { 390 | padding: 8, 391 | borderBottomColor: currentTheme.backgroundPrimary, 392 | borderBottomWidth: 1, 393 | flexDirection: 'column', 394 | }, 395 | repliedMessagePreviews: { 396 | paddingTop: 4, 397 | }, 398 | timestamp: { 399 | fontSize: 12, 400 | color: currentTheme.foregroundTertiary, 401 | position: 'relative', 402 | top: 2, 403 | left: 2, 404 | }, 405 | }); 406 | } 407 | export function setTheme(themeName: any) { 408 | currentThemeName = themeName; 409 | currentTheme = themes[themeName] ?? themes.Dark; 410 | refreshStyles(); 411 | } 412 | setTheme('Dark'); 413 | -------------------------------------------------------------------------------- /src/components/NetworkIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View} from 'react-native'; 3 | import {observer} from 'mobx-react-lite'; 4 | 5 | import {Client} from 'revolt.js'; 6 | 7 | import {currentTheme} from '../Theme'; 8 | import {Text} from './common/atoms'; 9 | 10 | export const NetworkIndicator = observer(({client}: {client: Client}) => { 11 | if (!client.user?.online) { 12 | return ( 13 | 24 | 30 | Connection lost 31 | 32 | 33 | ); 34 | } 35 | return <>; 36 | }); 37 | -------------------------------------------------------------------------------- /src/components/Notification.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {TouchableOpacity, View} from 'react-native'; 3 | import {observer} from 'mobx-react-lite'; 4 | 5 | import {Message} from 'revolt.js'; 6 | 7 | import {Avatar, Username} from '../Profile'; 8 | import {currentTheme} from '../Theme'; 9 | import {Text} from './common/atoms'; 10 | import {MarkdownView} from './common/MarkdownView'; 11 | import {parseRevoltNodes} from '../lib/utils'; 12 | 13 | export const Notification = observer( 14 | ({message, setState}: {message: Message | null; setState: any}) => { 15 | if (message) { 16 | return ( 17 | setState()}> 20 | 21 | 31 | 32 | 33 | 34 | 38 | 39 | {' '} 40 | ({message.channel?.name ?? message.channel?._id}) 41 | 42 | 43 | {message.content ? ( 44 | 45 | {parseRevoltNodes( 46 | message.content.length > 200 47 | ? message.content.slice(0, 200) + '...' 48 | : message.content, 49 | )} 50 | 51 | ) : ( 52 | 53 | Tap to view message 54 | 55 | )} 56 | 57 | 58 | 59 | 60 | ); 61 | } 62 | return <>; 63 | }, 64 | ); 65 | -------------------------------------------------------------------------------- /src/components/common/MarkdownView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import spoilerPlugin from '@traptitech/markdown-it-spoiler'; 4 | import Markdown, {hasParents, MarkdownIt} from 'react-native-markdown-display'; 5 | 6 | import {openUrl} from '../../Generic'; 7 | import {currentTheme} from '../../Theme'; 8 | import {Text} from './atoms'; 9 | 10 | const defaultMarkdownIt = MarkdownIt({typographer: true, linkify: true}) 11 | .disable(['image']) 12 | .use(spoilerPlugin); 13 | 14 | const spoilerStyle = { 15 | hiddenSpoiler: { 16 | backgroundColor: '#000', 17 | color: 'transparent', 18 | }, 19 | revealedSpoiler: { 20 | backgroundColor: currentTheme.backgroundSecondary, 21 | color: currentTheme.foregroundPrimary, 22 | }, 23 | }; 24 | 25 | const SpoilerContext = React.createContext(); 26 | const Spoiler = ({content}) => { 27 | const [revealed, setRevealed] = React.useState(false); 28 | return ( 29 | 30 | setRevealed(!revealed)}>{content} 31 | 32 | ); 33 | }; 34 | 35 | // the text and code_inline rules are the same as the built-in ones, 36 | // except with spoiler support 37 | const spoilerRule = { 38 | spoiler: (node, children) => , 39 | text: (node, children, parent, styles, inheritedStyles = {}) => { 40 | if (hasParents(parent, 'spoiler')) { 41 | return ( 42 | 43 | {isRevealed => ( 44 | 52 | {node.content} 53 | 54 | )} 55 | 56 | ); 57 | } 58 | 59 | return ( 60 | 61 | {node.content} 62 | 63 | ); 64 | }, 65 | code_inline: (node, children, parent, styles, inheritedStyles = {}) => { 66 | if (hasParents(parent, 'spoiler')) { 67 | return ( 68 | 69 | {isRevealed => ( 70 | 78 | {node.content} 79 | 80 | )} 81 | 82 | ); 83 | } 84 | 85 | return ( 86 | 87 | {node.content} 88 | 89 | ); 90 | }, 91 | }; 92 | 93 | export const MarkdownView = (props: any) => { 94 | let newProps = {...props}; 95 | if (!newProps.onLinkPress) { 96 | newProps = Object.assign({onLinkPress: openUrl}, newProps); 97 | } 98 | if (!newProps.markdownit) { 99 | newProps = Object.assign({markdownit: defaultMarkdownIt}, newProps); 100 | } 101 | if (!newProps.rules) { 102 | newProps = Object.assign({rules: spoilerRule}, newProps); 103 | } 104 | if (!newProps.style) { 105 | newProps = Object.assign({style: {}}, newProps); 106 | } 107 | if (!newProps.style.body) { 108 | newProps.style = Object.assign({body: {}}, newProps.style); 109 | } 110 | newProps.style.body = Object.assign( 111 | {color: currentTheme.foregroundPrimary}, 112 | newProps.style.body, 113 | ); 114 | if (!newProps.style.paragraph) { 115 | newProps.style = Object.assign({paragraph: {}}, newProps.style); 116 | } 117 | newProps.style.paragraph = Object.assign( 118 | {color: currentTheme.foregroundPrimary, marginTop: -3, marginBottom: 2}, 119 | newProps.style.paragraph, 120 | ); 121 | if (!newProps.style.link) { 122 | newProps.style = Object.assign({link: {}}, newProps.style); 123 | } 124 | newProps.style.link = Object.assign( 125 | {color: currentTheme.accentColor}, 126 | newProps.style.link, 127 | ); 128 | if (!newProps.style.code_inline) { 129 | newProps.style = Object.assign({code_inline: {}}, newProps.style); 130 | } 131 | newProps.style.code_inline = Object.assign( 132 | { 133 | color: currentTheme.foregroundPrimary, 134 | backgroundColor: currentTheme.backgroundSecondary, 135 | }, 136 | newProps.style.code_inline, 137 | ); 138 | if (!newProps.style.fence) { 139 | newProps.style = Object.assign({fence: {}}, newProps.style); 140 | } 141 | newProps.style.fence = Object.assign( 142 | { 143 | color: currentTheme.foregroundPrimary, 144 | backgroundColor: currentTheme.backgroundSecondary, 145 | borderWidth: 0, 146 | }, 147 | newProps.style.fence, 148 | ); 149 | if (!newProps.style.code_block) { 150 | newProps.style = Object.assign({code_block: {}}, newProps.style); 151 | } 152 | newProps.style.code_block = Object.assign( 153 | { 154 | borderColor: currentTheme.foregroundPrimary, 155 | color: currentTheme.foregroundPrimary, 156 | backgroundColor: currentTheme.backgroundSecondary, 157 | }, 158 | newProps.style.code_block, 159 | ); 160 | if (!newProps.style.blockquote) { 161 | newProps.style = Object.assign({blockquote: {}}, newProps.style); 162 | } 163 | newProps.style.blockquote = Object.assign( 164 | { 165 | marginBottom: 2, 166 | paddingVertical: 6, 167 | borderRadius: 4, 168 | borderColor: currentTheme.foregroundPrimary, 169 | color: currentTheme.foregroundPrimary, 170 | backgroundColor: currentTheme.blockQuoteBackground, 171 | }, 172 | newProps.style.block_quote, 173 | ); 174 | try { 175 | return {newProps.children}; 176 | } catch (e) { 177 | return Error rendering markdown; 178 | } 179 | }; 180 | -------------------------------------------------------------------------------- /src/components/common/atoms/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {TouchableOpacity} from 'react-native'; 3 | 4 | import {styles} from '../../../Theme'; 5 | 6 | type ButtonProps = { 7 | children?: any; 8 | backgroundColor?: string; 9 | onPress?: any; 10 | onLongPress?: any; 11 | delayLongPress?: number; 12 | style?: any; 13 | }; 14 | 15 | export function Button({ 16 | children, 17 | backgroundColor, 18 | onPress, 19 | onLongPress, 20 | delayLongPress, 21 | style, 22 | ...props 23 | }: ButtonProps) { 24 | return ( 25 | 31 | {children} 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/common/atoms/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {TouchableOpacity} from 'react-native'; 3 | import {observer} from 'mobx-react-lite'; 4 | 5 | import {currentTheme} from '../../../Theme'; 6 | 7 | import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; 8 | 9 | import {Text} from './Text'; 10 | 11 | export const Checkbox = observer( 12 | ({value, callback}: {value: boolean; callback: any}) => { 13 | return ( 14 | 26 | 32 | {value ? ( 33 | 38 | ) : null} 39 | 40 | 41 | ); 42 | }, 43 | ); 44 | -------------------------------------------------------------------------------- /src/components/common/atoms/ContextButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {TouchableOpacity} from 'react-native'; 3 | 4 | import {styles} from '../../../Theme'; 5 | 6 | type ButtonProps = { 7 | children?: any; 8 | backgroundColor?: string; 9 | onPress?: any; 10 | onLongPress?: any; 11 | delayLongPress?: number; 12 | style?: any; 13 | }; 14 | 15 | export function ContextButton({ 16 | children, 17 | backgroundColor, 18 | onPress, 19 | onLongPress, 20 | delayLongPress, 21 | style, 22 | ...props 23 | }: ButtonProps) { 24 | return ( 25 | 35 | {children} 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/common/atoms/CopyIDButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View} from 'react-native'; 3 | 4 | import Clipboard from '@react-native-clipboard/clipboard'; 5 | import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; 6 | 7 | import {currentTheme, styles} from '../../../Theme'; 8 | import {ContextButton} from './ContextButton'; 9 | import {Text} from './Text'; 10 | 11 | export const CopyIDButton = ({id}: {id: string}) => { 12 | return ( 13 | { 16 | Clipboard.setString(id); 17 | }}> 18 | 19 | 24 | 25 | 26 | Copy ID ({id}) 27 | 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/common/atoms/Link.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {TouchableOpacity} from 'react-native'; 3 | 4 | import {openUrl} from '../../../Generic'; 5 | import {styles} from '../../../Theme'; 6 | import {Text} from './Text'; 7 | 8 | type LinkProps = { 9 | link: string; 10 | label: string; 11 | style?: any; 12 | }; 13 | 14 | export const Link = ({link, label, style}: LinkProps) => { 15 | let finalStyle = styles.link; 16 | if (style) { 17 | finalStyle = Object.assign({}, finalStyle, style); 18 | } 19 | 20 | return ( 21 | openUrl(link)}> 22 | {label} 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/common/atoms/Text.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactNative from 'react-native'; 3 | 4 | import {currentTheme, styles} from '../../../Theme'; 5 | 6 | export const Text = (props: any) => { 7 | let newProps = {...props}; 8 | if (!props.style) { 9 | newProps = Object.assign({style: {}}, newProps); 10 | } 11 | const font = props.useInter ? 'Inter' : 'Open Sans'; 12 | if (props.type) { 13 | switch (props.type) { 14 | case 'header': 15 | newProps.style = Object.assign({}, styles.headerv2, newProps.style); 16 | break; 17 | default: 18 | break; 19 | } 20 | } 21 | if (props.colour) { 22 | newProps.style.color = props.colour; 23 | } 24 | newProps.style = Object.assign( 25 | { 26 | color: currentTheme.foregroundPrimary, 27 | flexWrap: 'wrap', 28 | fontFamily: font, 29 | }, 30 | newProps.style, 31 | ); 32 | return {newProps.children}; 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/common/atoms/index.tsx: -------------------------------------------------------------------------------- 1 | export {Button} from './Button'; 2 | export {Checkbox} from './Checkbox'; 3 | export {ContextButton} from './ContextButton'; 4 | export {CopyIDButton} from './CopyIDButton'; 5 | export {Link} from './Link'; 6 | export {Text} from './Text'; 7 | -------------------------------------------------------------------------------- /src/components/common/messaging/InviteEmbed.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from 'react'; 2 | import {View} from 'react-native'; 3 | import {observer} from 'mobx-react-lite'; 4 | 5 | import {API, Message} from 'revolt.js'; 6 | 7 | import {GeneralAvatar, app, client} from '../../../Generic'; 8 | import {currentTheme} from '../../../Theme'; 9 | import {Button, Text} from '../atoms'; 10 | 11 | export const InviteEmbed = observer( 12 | ({message, invite}: {message: Message; invite: string}) => { 13 | const [invObject, setInvObject] = React.useState({} as API.InviteResponse); 14 | const [error, setError] = React.useState(''); 15 | 16 | useEffect(() => { 17 | async function getInv() { 18 | try { 19 | const i = await client.fetchInvite(invite); 20 | setInvObject(i); 21 | } catch (e) { 22 | const errorType = (e as string).match('404') 23 | ? 'notFound' 24 | : (e as string).match('429') 25 | ? 'rateLimited' 26 | : 'otherError'; 27 | if (errorType === 'otherError') { 28 | console.warn( 29 | `[INVITEEMBED] Unrecognised error fetching invite: ${e}`, 30 | ); 31 | } else { 32 | console.log(`[INVITEEMBED] Error fetching invite: ${e}`); 33 | } 34 | setError(errorType); 35 | } 36 | } 37 | getInv(); 38 | }, [invite]); 39 | 40 | return error ? ( 41 | 48 | error: {error} 49 | 50 | ) : invObject.type === 'Server' ? ( 51 | 58 | 61 | 66 | {message.author?.username} 67 | {' '} 68 | invited you to a server 69 | 70 | 71 | 76 | 83 | 84 | {invObject.server_name} 85 | 86 | 87 | {invObject?.member_count}{' '} 88 | {invObject?.member_count === 1 ? 'member' : 'members'} 89 | 90 | 91 | 92 | 105 | 106 | ) : ( 107 | Invite: {invite} 108 | ); 109 | }, 110 | ); 111 | -------------------------------------------------------------------------------- /src/components/common/messaging/MessageEmbed.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Dimensions, Pressable, View} from 'react-native'; 3 | import {observer} from 'mobx-react-lite'; 4 | 5 | import FastImage from 'react-native-fast-image'; 6 | 7 | import {API} from 'revolt.js'; 8 | 9 | import {app, client} from '../../../Generic'; 10 | import {currentTheme} from '../../../Theme'; 11 | import {MarkdownView} from '../MarkdownView'; 12 | import {Link, Text} from '../atoms'; 13 | const Image = FastImage; 14 | 15 | export const MessageEmbed = observer((eRaw: API.Embed) => { 16 | // @ts-expect-error This seems to be necessary even though it clashses with the API types 17 | const e = eRaw.embed; 18 | switch (e.type) { 19 | case 'Text': 20 | case 'Website': 21 | return ( 22 | 28 | {e.type === 'Website' && e.site_name ? ( 29 | 32 | {e.site_name} 33 | 34 | ) : null} 35 | {e.title && e.url ? ( 36 | 41 | ) : ( 42 | 47 | {e.title} 48 | 49 | )} 50 | {e.description ? {e.description} : null} 51 | {(() => { 52 | if (e.type === 'Website' && e.image) { 53 | let width = e.image.width; 54 | let height = e.image.height; 55 | if (width > Dimensions.get('screen').width - 82) { 56 | let sizeFactor = (Dimensions.get('screen').width - 82) / width; 57 | width = width * sizeFactor; 58 | height = height * sizeFactor; 59 | } 60 | return ( 61 | app.openImage(e.image?.url)}> 62 | 71 | 72 | ); 73 | } 74 | })()} 75 | 76 | ); 77 | case 'Image': 78 | // if (e.image?.size === "Large") {} 79 | let width = e.width; 80 | let height = e.height; 81 | if (width > Dimensions.get('screen').width - 75) { 82 | let sizeFactor = (Dimensions.get('screen').width - 75) / width; 83 | width = width * sizeFactor; 84 | height = height * sizeFactor; 85 | } 86 | return ( 87 | app.openImage(client.proxyFile(e.url))}> 88 | 97 | 98 | ); 99 | case 'Video': 100 | return ( 101 | 102 | Video embeds are not currently supported 103 | 104 | ); 105 | case 'None': 106 | default: 107 | console.log(JSON.stringify(e)); 108 | return ( 109 | 110 | embed - type: {e.type === 'None' ? 'none' : e.type ?? 'how'}, other 111 | info: 112 | {JSON.stringify(e)} 113 | 114 | ); 115 | } 116 | }); 117 | -------------------------------------------------------------------------------- /src/components/common/messaging/ReplyMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View} from 'react-native'; 3 | 4 | import {Message} from 'revolt.js'; 5 | 6 | import {Avatar, Username} from '../../../Profile'; 7 | import {styles} from '../../../Theme'; 8 | import {Text} from '../atoms'; 9 | 10 | type ReplyProps = { 11 | message?: Message; 12 | mention?: boolean; 13 | style?: any; 14 | }; 15 | 16 | export const ReplyMessage = (props: ReplyProps) => { 17 | if (!props.message?.system) { 18 | return ( 19 | 20 | 21 | {props.message ? ( 22 | props.message.author ? ( 23 | <> 24 | 30 | {props.mention ? '@' : ''} 31 | 36 | 37 | {props.message.content?.split('\n').join(' ')} 38 | 39 | 40 | ) : null 41 | ) : ( 42 | Message not loaded 43 | )} 44 | 45 | ); 46 | } else { 47 | return <>; 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/common/messaging/index.ts: -------------------------------------------------------------------------------- 1 | export {InviteEmbed} from './InviteEmbed'; 2 | export {Message} from './Message'; 3 | export {MessageEmbed} from './MessageEmbed'; 4 | export {ReplyMessage} from './ReplyMessage'; -------------------------------------------------------------------------------- /src/components/navigation/ChannelHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View, TouchableOpacity} from 'react-native'; 3 | 4 | import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; 5 | 6 | import {app} from '../../Generic'; 7 | import {currentTheme, styles} from '../../Theme'; 8 | 9 | export const ChannelHeader = ({children}: {children: any}) => { 10 | return ( 11 | 12 | { 15 | app.openLeftMenu(); 16 | }}> 17 | 18 | 23 | 24 | 25 | {children} 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/navigation/ChannelList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {TouchableOpacity, View} from 'react-native'; 3 | import {observer} from 'mobx-react-lite'; 4 | 5 | import FastImage from 'react-native-fast-image'; 6 | 7 | import {Channel, Server} from 'revolt.js'; 8 | 9 | import {ChannelButton, app, client} from '../../Generic'; 10 | import {styles} from '../../Theme'; 11 | import {Text} from '../common/atoms'; 12 | const Image = FastImage; 13 | 14 | type ChannelListProps = { 15 | onChannelClick: any; 16 | currentChannel: Channel; 17 | currentServer: Server; 18 | }; 19 | 20 | // thanks a lot, revolt.js 🙄 21 | type Category = { 22 | id: string; 23 | title: string; 24 | channels: string[]; 25 | }; 26 | 27 | const ServerChannelListCategory = observer( 28 | ({category, props}: {category: Category; props: ChannelListProps}) => { 29 | const [isVisible, setIsVisible] = React.useState(true); 30 | return ( 31 | 32 | { 35 | setIsVisible(!isVisible); 36 | }}> 37 | 43 | {category.title?.toUpperCase()} 44 | 45 | 46 | {isVisible && 47 | category.channels.map((cid: string) => { 48 | let c = client.channels.get(cid); 49 | if (c) { 50 | return ( 51 | { 55 | props.onChannelClick(c); 56 | }} 57 | selected={props.currentChannel?._id === c._id} 58 | /> 59 | ); 60 | } 61 | })} 62 | 63 | ); 64 | }, 65 | ); 66 | 67 | const ServerChannelList = observer((props: ChannelListProps) => { 68 | const [processedChannels, setProcessedChannels] = React.useState( 69 | [] as string[], 70 | ); 71 | const [res, setRes] = React.useState([] as JSX.Element[] | undefined); 72 | 73 | React.useEffect(() => { 74 | let categories = props.currentServer.categories?.map(c => { 75 | const element = ( 76 | 81 | ); 82 | for (const cnl of c.channels) { 83 | if (!processedChannels.includes(cnl)) { 84 | let newProcessedChannels = processedChannels; 85 | newProcessedChannels.push(cnl); 86 | setProcessedChannels(newProcessedChannels); 87 | } 88 | } 89 | return element; 90 | }); 91 | setRes(categories); 92 | }, [props, processedChannels]); 93 | 94 | return ( 95 | <> 96 | {props.currentServer.banner ? ( 97 | 100 | app.openServerContextMenu(props.currentServer)}> 102 | {props.currentServer.name} 103 | 104 | 105 | ) : ( 106 | app.openServerContextMenu(props.currentServer)}> 108 | {props.currentServer.name} 109 | 110 | )} 111 | 112 | {props.currentServer.channels.map(c => { 113 | if (c) { 114 | if (!processedChannels.includes(c._id)) { 115 | return ( 116 | { 120 | props.onChannelClick(c); 121 | }} 122 | selected={props.currentChannel?._id === c._id} 123 | /> 124 | ); 125 | } 126 | } 127 | })} 128 | {res} 129 | 130 | ); 131 | }); 132 | 133 | export const ChannelList = observer((props: ChannelListProps) => { 134 | return ( 135 | <> 136 | {!props.currentServer ? ( 137 | <> 138 | 145 | Direct Messages 146 | 147 | 148 | { 150 | props.onChannelClick(null); 151 | }} 152 | key={'home'} 153 | channel={'Home'} 154 | selected={props.currentChannel === null} 155 | /> 156 | 157 | { 159 | props.onChannelClick('friends'); 160 | }} 161 | key={'friends'} 162 | channel={'Friends'} 163 | selected={props.currentChannel === 'friends'} 164 | /> 165 | 166 | { 168 | props.onChannelClick(await client.user?.openDM()); 169 | }} 170 | key={'notes'} 171 | channel={'Saved Notes'} 172 | selected={props.currentChannel?.channel_type === 'SavedMessages'} 173 | /> 174 | 175 | {__DEV__ ? ( 176 | { 178 | await client.user?.openDM(); 179 | props.onChannelClick('debug'); 180 | }} 181 | key={'debugChannel'} 182 | channel={'Debug'} 183 | selected={props.currentChannel === 'debug'} 184 | /> 185 | ) : null} 186 | 187 | {[...client.channels.values()] 188 | .filter( 189 | c => 190 | c.channel_type === 'DirectMessage' || 191 | c.channel_type === 'Group', 192 | ) 193 | .sort((c1, c2) => c2.updatedAt - c1.updatedAt) 194 | .map(dm => ( 195 | { 197 | props.onChannelClick(dm); 198 | }} 199 | onLongPress={() => { 200 | app.openProfile(dm.recipient); 201 | }} 202 | delayLongPress={750} 203 | key={dm._id} 204 | channel={dm} 205 | style={props.currentChannel?._id === dm._id} 206 | /> 207 | ))} 208 | 209 | ) : null} 210 | {props.currentServer ? ( 211 | 216 | ) : null} 217 | 218 | ); 219 | }); 220 | -------------------------------------------------------------------------------- /src/components/navigation/ServerList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {TouchableOpacity, View} from 'react-native'; 3 | import {observer} from 'mobx-react-lite'; 4 | 5 | import FastImage from 'react-native-fast-image'; 6 | import MaterialCommunityIcon from 'react-native-vector-icons/MaterialCommunityIcons'; 7 | 8 | import {client, openUrl} from '../../Generic'; 9 | import {currentTheme, styles} from '../../Theme'; 10 | import {Text} from '../common/atoms'; 11 | import {DEFAULT_MAX_SIDE} from '../../lib/consts'; 12 | 13 | const Image = FastImage; 14 | 15 | export const ServerList = observer( 16 | ({ 17 | onServerPress, 18 | onServerLongPress, 19 | filter, 20 | ordered, 21 | showUnread = true, 22 | showDiscover = true, 23 | }: { 24 | onServerPress: any; 25 | onServerLongPress?: any; 26 | ordered?: string[]; 27 | filter?: any; 28 | showUnread?: boolean; 29 | showDiscover?: boolean; 30 | }) => { 31 | let servers = [...client.servers.values()]; 32 | if (filter) { 33 | servers = servers.filter(filter); 34 | } 35 | if (ordered) { 36 | servers.sort((server1, server2) => { 37 | return ordered.indexOf(server1._id) - ordered.indexOf(server2._id); 38 | }); 39 | } 40 | return ( 41 | 42 | {servers.map(s => { 43 | let iconURL = s.generateIconURL(); 44 | let pings = s.getMentions().length; 45 | let initials = ''; 46 | for (const word of s.name.split(' ')) { 47 | initials += word.charAt(0); 48 | } 49 | return ( 50 | 51 | {showUnread && s.getMentions().length > 0 ? ( 52 | 65 | 68 | {pings > 9 ? '9+' : pings} 69 | 70 | 71 | ) : showUnread && s.isUnread() ? ( 72 | 86 | ) : null} 87 | { 89 | onServerPress(s); 90 | }} 91 | onLongPress={() => { 92 | onServerLongPress(s); 93 | }} 94 | key={s._id} 95 | style={styles.serverButton}> 96 | {iconURL ? ( 97 | 123 | { 125 | openUrl('https://rvlt.gg/discover'); 126 | }} 127 | key={'serverlist-discover'} 128 | style={styles.serverButton}> 129 | 130 | 135 | 136 | 137 | 138 | ) : null} 139 | 140 | ); 141 | }, 142 | ); 143 | -------------------------------------------------------------------------------- /src/components/navigation/UserList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View} from 'react-native'; 3 | import {observer} from 'mobx-react-lite'; 4 | 5 | import {User} from 'revolt.js'; 6 | 7 | import {app} from '../../Generic'; 8 | import {MiniProfile} from '../../Profile'; 9 | import {currentTheme} from '../../Theme'; 10 | 11 | import {Button} from '../common/atoms'; 12 | 13 | export const UserList = observer(({users}: {users: User[]}) => { 14 | return ( 15 | <> 16 | {users.map(u => ( 17 | 33 | ))} 34 | 35 | ); 36 | }); 37 | -------------------------------------------------------------------------------- /src/components/pages/FriendsPage.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {ScrollView, TouchableOpacity, View} from 'react-native'; 3 | import {observer} from 'mobx-react-lite'; 4 | 5 | import {client, ChannelIcon, app} from '../../Generic'; 6 | import {MiniProfile} from '../../Profile'; 7 | import {styles} from '../../Theme'; 8 | import {ChannelHeader} from '../navigation/ChannelHeader'; 9 | import {Button, Text} from '../common/atoms'; 10 | 11 | type DisplayStates = { 12 | onlineFriends: boolean; 13 | offlineFriends: boolean; 14 | incoming: boolean; 15 | outgoing: boolean; 16 | blocked: boolean; 17 | }; 18 | 19 | export const FriendsPage = observer(() => { 20 | const sectionHeaderStyles = { 21 | fontWeight: 'bold', 22 | margin: 5, 23 | marginLeft: 10, 24 | marginTop: 10, 25 | }; 26 | 27 | const [displayState, setDisplayState] = useState({ 28 | onlineFriends: true, 29 | offlineFriends: true, 30 | outgoing: true, 31 | incoming: true, 32 | blocked: true, 33 | } as DisplayStates); 34 | 35 | // sort the user list, then filter for friends/blocked users/outgoing/incoming friend requests and render the buttons 36 | const sortedUsers = [...client.users.values()].sort((f1, f2) => 37 | f1.username.localeCompare(f2.username), 38 | ); 39 | 40 | const onlineFriends = [] as JSX.Element[]; 41 | const offlineFriends = [] as JSX.Element[]; 42 | const incoming = [] as JSX.Element[]; 43 | const outgoing = [] as JSX.Element[]; 44 | const blocked = [] as JSX.Element[]; 45 | 46 | for (const u of sortedUsers) { 47 | switch (u.relationship) { 48 | case 'Friend': 49 | (u.online ? onlineFriends : offlineFriends).push( 50 | , 56 | ); 57 | break; 58 | case 'Incoming': 59 | incoming.push( 60 | , 66 | ); 67 | break; 68 | case 'Outgoing': 69 | outgoing.push( 70 | , 76 | ); 77 | break; 78 | case 'Blocked': 79 | blocked.push( 80 | , 86 | ); 87 | break; 88 | default: 89 | break; 90 | } 91 | } 92 | 93 | return ( 94 | 95 | 96 | 97 | 98 | 99 | Friends 100 | 101 | 102 | {/* incoming requests */} 103 | 105 | setDisplayState({ 106 | ...displayState, 107 | incoming: !displayState.incoming, 108 | }) 109 | }> 110 | 111 | INCOMING REQUESTS - {incoming.length} 112 | 113 | 114 | {displayState.incoming && ( 115 | 116 | {incoming.length > 0 ? ( 117 | incoming 118 | ) : ( 119 | No incoming requests 120 | )} 121 | 122 | )} 123 | 124 | {/* outgoing requests */} 125 | 127 | setDisplayState({ 128 | ...displayState, 129 | outgoing: !displayState.outgoing, 130 | }) 131 | }> 132 | 133 | OUTGOING REQUESTS - {outgoing.length} 134 | 135 | 136 | {displayState.outgoing && ( 137 | 138 | {outgoing.length > 0 ? ( 139 | outgoing 140 | ) : ( 141 | No outgoing requests 142 | )} 143 | 144 | )} 145 | 146 | {/* online friends */} 147 | 149 | setDisplayState({ 150 | ...displayState, 151 | onlineFriends: !displayState.onlineFriends, 152 | }) 153 | }> 154 | 155 | ONLINE FRIENDS - {onlineFriends.length} 156 | 157 | 158 | {displayState.onlineFriends && ( 159 | 160 | {onlineFriends.length > 0 ? ( 161 | onlineFriends 162 | ) : ( 163 | No online friends 164 | )} 165 | 166 | )} 167 | 168 | {/* offline friends */} 169 | 171 | setDisplayState({ 172 | ...displayState, 173 | offlineFriends: !displayState.offlineFriends, 174 | }) 175 | }> 176 | 177 | OFFLINE FRIENDS - {offlineFriends.length} 178 | 179 | 180 | {displayState.offlineFriends && ( 181 | 182 | {offlineFriends.length > 0 ? ( 183 | offlineFriends 184 | ) : ( 185 | No offline friends 186 | )} 187 | 188 | )} 189 | 190 | {/* blocked users */} 191 | 193 | setDisplayState({ 194 | ...displayState, 195 | blocked: !displayState.blocked, 196 | }) 197 | }> 198 | BLOCKED - {blocked.length} 199 | 200 | {displayState.blocked && ( 201 | 202 | {blocked.length > 0 ? ( 203 | blocked 204 | ) : ( 205 | No blocked users 206 | )} 207 | 208 | )} 209 | 210 | 211 | ); 212 | }); 213 | -------------------------------------------------------------------------------- /src/components/pages/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {TouchableOpacity, View} from 'react-native'; 3 | import {observer} from 'mobx-react-lite'; 4 | 5 | import {app, ChannelIcon, client, openUrl} from '../../Generic'; 6 | import {Avatar, Username} from '../../Profile'; 7 | import { 8 | SPECIAL_DATES, 9 | SPECIAL_DATE_OBJECTS, 10 | SPECIAL_SERVERS, 11 | } from '../../lib/consts'; 12 | import {styles} from '../../Theme'; 13 | import {ChannelHeader} from '../navigation/ChannelHeader'; 14 | import {Button, Text} from '../common/atoms'; 15 | 16 | export const HomePage = observer(() => { 17 | // holiday emoji 18 | const rawDate = new Date(); 19 | const rawMonth = rawDate.getMonth() + 1; 20 | const date = `${rawDate.getDate()}/${rawMonth}`; 21 | const month = `month${rawMonth}`; 22 | 23 | let holidayEmoji = SPECIAL_DATES.includes(date) ? ( 24 | { 26 | // @ts-expect-error TODO: figure out types for this 27 | openUrl(SPECIAL_DATE_OBJECTS[date].link); 28 | }}> 29 | 33 | { 34 | // @ts-expect-error as above 35 | SPECIAL_DATE_OBJECTS[date].emoji 36 | } 37 | 38 | 39 | ) : SPECIAL_DATES.includes(month) ? ( 40 | { 42 | // @ts-expect-error as above 43 | openUrl(SPECIAL_DATE_OBJECTS[month].link); 44 | }}> 45 | 48 | { 49 | // @ts-expect-error as above 50 | SPECIAL_DATE_OBJECTS[month].emoji 51 | } 52 | 53 | 54 | ) : null; 55 | 56 | return ( 57 | <> 58 | 59 | 60 | 61 | 62 | Home 63 | 64 | 71 | { 78 | let user = client.users.get(client.user?._id!); 79 | if (user) { 80 | app.openProfile(user); 81 | } 82 | }}> 83 | 84 | 85 | 86 | 87 | 88 | RVMob 89 | 90 | 93 | Swipe from the left of the screen or press the three lines icon to see 94 | your servers and messages! 95 | 96 | 105 | 116 | 122 | {app.settings.get('ui.home.holidays') ? holidayEmoji : null} 123 | 124 | 125 | ); 126 | }); 127 | -------------------------------------------------------------------------------- /src/components/pages/LoginSettingsPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {TextInput, View} from 'react-native'; 3 | 4 | import {app} from '../../Generic'; 5 | import {currentTheme, styles} from '../../Theme'; 6 | import {Button, Text} from '../common/atoms'; 7 | 8 | export const LoginSettingsPage = ({state}: {state: any}) => { 9 | const [instanceURL, setInstanceURL] = React.useState( 10 | (app.settings.get('app.instance') as string) ?? '', 11 | ); 12 | const [testResponse, setTestResponse] = React.useState(null as string | null); 13 | 14 | const [saved, setSaved] = React.useState(false); 15 | 16 | async function testURL(url: string, returnIfSuccessful: boolean) { 17 | try { 18 | console.log('[LOGINSETTINGS] Testing URL...'); 19 | const req = await fetch(url); 20 | console.log('[LOGINSETTINGS] Request succeeded!'); 21 | const data = await req.json(); 22 | if (data.revolt && data.features) { 23 | console.log('[LOGINSETTINGS] This looks like a Revolt instance!'); 24 | setTestResponse('valid'); 25 | if (returnIfSuccessful) { 26 | return true; 27 | } 28 | } else { 29 | console.log( 30 | "[LOGINSETTINGS] This doesn't look like a Revolt instance...", 31 | ); 32 | setTestResponse('invalid'); 33 | } 34 | } catch (err: any) { 35 | if (err.toString().match('Network request failed')) { 36 | console.log(`[LOGINSETTINGS] Could not fetch ${instanceURL}`); 37 | setTestResponse('requestFailed'); 38 | } else if (err.toString().match('JSON Parse error')) { 39 | console.log( 40 | "[LOGINSETTINGS] Could not parse the response (it's probably HTML)", 41 | ); 42 | setTestResponse('notJSON'); 43 | } else { 44 | console.log(err); 45 | setTestResponse('error'); 46 | } 47 | } 48 | } 49 | 50 | return ( 51 | 52 | 53 | {saved ? ( 54 | <> 55 | Saved! 56 | 57 | For now, you'll have to close and reopen the app for your changes 58 | to apply. We know this isn't ideal - we'll fix this at a later 59 | date. 60 | 61 | 62 | ) : ( 63 | <> 64 | Instance 65 | { 70 | setInstanceURL(text); 71 | }} 72 | value={instanceURL} 73 | /> 74 | 75 | {testResponse 76 | ? testResponse === 'valid' 77 | ? 'This looks like a Revolt instance!' 78 | : testResponse === 'invalid' 79 | ? "This doesn't look like a Revolt instance..." 80 | : testResponse === 'notJSON' 81 | ? "Could not parse response - make sure you're linking to the API URL" 82 | : testResponse === 'requestFailed' 83 | ? 'Could not fetch that URL' 84 | : 'Something went wrong!' 85 | : null} 86 | 87 | 93 | 105 | 106 | )} 107 | 108 | 109 | ); 110 | }; 111 | -------------------------------------------------------------------------------- /src/components/pages/VoiceChannel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {View} from 'react-native'; 3 | 4 | import {styles} from '../../Theme'; 5 | import {Text} from '../common/atoms'; 6 | 7 | export const VoiceChannel = () => { 8 | return ( 9 | 16 | 17 | Voice channels aren't supported in RVMob yet! 18 | 19 | 20 | In the meantime, you can join them via the web app or Revolt Desktop. 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/sheets/ChannelInfoSheet.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {ScrollView, View} from 'react-native'; 3 | import {observer} from 'mobx-react-lite'; 4 | 5 | import {Channel, User} from 'revolt.js'; 6 | 7 | import {currentTheme} from '../../Theme'; 8 | import {Text} from '../common/atoms'; 9 | import {MarkdownView} from '../common/MarkdownView'; 10 | 11 | export const ChannelInfoSheet = observer(({channel}: {channel: Channel}) => { 12 | const [groupMembers, setGroupMembers] = React.useState([] as User[]); 13 | 14 | React.useEffect(() => { 15 | async function fetchMembers() { 16 | const m = 17 | channel.channel_type === 'Group' ? await channel.fetchMembers() : []; 18 | setGroupMembers(m); 19 | } 20 | fetchMembers(); 21 | }, [channel]); 22 | return ( 23 | 24 | 25 | 31 | {channel.name} 32 | 33 | 38 | {channel.channel_type === 'Group' 39 | ? `Group (${groupMembers.length} ${ 40 | groupMembers.length === 1 ? 'member' : 'members' 41 | })` 42 | : 'Regular channel'} 43 | 44 | {channel.description ? ( 45 | 51 | 57 | {channel.description} 58 | 59 | 60 | ) : null} 61 | 62 | 63 | ); 64 | }); 65 | -------------------------------------------------------------------------------- /src/components/sheets/MemberListSheet.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {ScrollView, View} from 'react-native'; 3 | import {observer} from 'mobx-react-lite'; 4 | 5 | import {Channel, Member, Server, User} from 'revolt.js'; 6 | 7 | import {Text} from '../common/atoms'; 8 | import {UserList} from '../navigation/UserList'; 9 | 10 | interface ServerMemberList { 11 | context: Server; 12 | users: User[]; // Member[]; 13 | } 14 | 15 | interface ChannelMemberList { 16 | context: Channel; 17 | users: User[]; 18 | } 19 | 20 | export const MemberListSheet = observer( 21 | ({context, users}: ServerMemberList | ChannelMemberList) => { 22 | return ( 23 | 24 | {context.name ?? context._id} members 25 | 26 | 27 | 28 | ); 29 | }, 30 | ); 31 | -------------------------------------------------------------------------------- /src/components/sheets/MessageMenuSheet.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {ScrollView, View} from 'react-native'; 3 | import {observer} from 'mobx-react-lite'; 4 | 5 | import Clipboard from '@react-native-clipboard/clipboard'; 6 | import MaterialCommunityIcon from 'react-native-vector-icons/MaterialCommunityIcons'; 7 | import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; 8 | 9 | import {Message} from 'revolt.js'; 10 | 11 | import {app} from '../../Generic'; 12 | import {currentTheme, styles} from '../../Theme'; 13 | import {ContextButton, CopyIDButton, Text} from '../common/atoms'; 14 | import {ReplyMessage} from '../common/messaging'; 15 | 16 | export const MessageMenuSheet = observer( 17 | ({state, message}: {state: any; message: Message}) => { 18 | return ( 19 | <> 20 | 21 | 22 | state.setState({contextMenuMessage: null})}> 24 | 25 | 30 | 31 | Close 32 | 33 | {message?.channel?.havePermission('SendMessage') ? ( 34 | { 36 | let replyingMessages = [...app.getReplyingMessages()]; 37 | if ( 38 | replyingMessages.filter(m => m.message._id === message._id) 39 | .length > 0 40 | ) { 41 | return; 42 | } 43 | if (replyingMessages.length >= 5) { 44 | return; 45 | } 46 | if (app.getEditingMessage()) { 47 | return; 48 | } 49 | replyingMessages.push({ 50 | message: message, 51 | mentions: false, 52 | }); 53 | app.setReplyingMessages(replyingMessages); 54 | state.setState({contextMenuMessage: null}); 55 | }}> 56 | 57 | 62 | 63 | Reply 64 | 65 | ) : null} 66 | {message.content ? ( 67 | { 69 | Clipboard.setString(message.content!); 70 | }}> 71 | 72 | 77 | 78 | Copy content 79 | 80 | ) : null} 81 | {app.settings.get('ui.showDeveloperFeatures') ? ( 82 | 83 | ) : null} 84 | {message?.channel?.havePermission('ManageMessages') || 85 | message?.author?.relationship === 'User' ? ( 86 | { 88 | message.delete(); 89 | state.setState({contextMenuMessage: null}); 90 | }}> 91 | 92 | 97 | 98 | Delete 99 | 100 | ) : null} 101 | {message?.author?.relationship === 'User' ? ( 102 | { 104 | app.setMessageBoxInput(message?.content); 105 | app.setEditingMessage(message); 106 | app.setReplyingMessages([]); 107 | state.setState({contextMenuMessage: null}); 108 | }}> 109 | 110 | 115 | 116 | Edit 117 | 118 | ) : null} 119 | {message?.author?.relationship !== 'User' ? ( 120 | { 122 | app.openReportMenu(message, 'Message'); 123 | state.setState({contextMenuMessage: null}); 124 | }}> 125 | 126 | 131 | 132 | Report Message 133 | 134 | ) : null} 135 | 136 | 137 | 138 | ); 139 | }, 140 | ); 141 | -------------------------------------------------------------------------------- /src/components/sheets/ServerInfoSheet.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from 'react'; 2 | import {ScrollView, View} from 'react-native'; 3 | import {observer} from 'mobx-react-lite'; 4 | 5 | import FastImage from 'react-native-fast-image'; 6 | import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; 7 | 8 | import {Member, Server, User} from 'revolt.js'; 9 | 10 | import {GeneralAvatar, app, client} from '../../Generic'; 11 | import {SPECIAL_SERVERS} from '../../lib/consts'; 12 | import {currentTheme, styles} from '../../Theme'; 13 | import {ContextButton, CopyIDButton, Text} from '../common/atoms'; 14 | import {MarkdownView} from '../common/MarkdownView'; 15 | 16 | const Image = FastImage; 17 | 18 | export const ServerInfoSheet = observer( 19 | ({state, server}: {state: any; server: Server}) => { 20 | const [members, setMembers] = React.useState( 21 | {} as {members: Member[]; users: User[]}, 22 | ); 23 | 24 | useEffect(() => { 25 | async function fetchMembers() { 26 | if (server._id !== SPECIAL_SERVERS.lounge.id) { 27 | const start = new Date().getTime(); 28 | console.log(`[SERVERINFOSHEET] Fetching members... (${start})`); 29 | const m = await server.fetchMembers(); 30 | const mid = new Date().getTime(); 31 | console.log(`[SERVERINFOSHEET] Fetched members (${mid})`); 32 | setMembers(m); 33 | const end = new Date().getTime(); 34 | console.log(`[SERVERINFOSHEET] Set members (${end})`); 35 | } 36 | } 37 | fetchMembers(); 38 | }, [server]); 39 | 40 | return ( 41 | 42 | 43 | {server.banner ? ( 44 | 48 | ) : null} 49 | {server.icon ? ( 50 | 51 | ) : null} 52 | 58 | {server.name} 59 | 60 | 65 | {server._id === SPECIAL_SERVERS.lounge.id 66 | ? 'Member count disabled for this server' 67 | : members.members 68 | ? `${members.members.length} ${ 69 | members.members.length === 1 ? 'member' : 'members' 70 | }` 71 | : 'Fetching member count...'} 72 | 73 | {server.description ? ( 74 | 80 | 86 | {server.description} 87 | 88 | 89 | ) : null} 90 | 91 | 96 | {app.settings.get('ui.showDeveloperFeatures') ? ( 97 | 98 | ) : null} 99 | {server.owner !== client.user?._id ? ( 100 | <> 101 | { 104 | await app.openServer(); 105 | state.setState({contextMenuServer: null}); 106 | server.delete(); 107 | }}> 108 | 109 | 114 | 115 | Leave Server 116 | 117 | { 120 | app.openReportMenu(server, 'Server'); 121 | }}> 122 | 123 | 128 | 129 | Report Server 130 | 131 | 132 | ) : null} 133 | 134 | 135 | ); 136 | }, 137 | ); 138 | -------------------------------------------------------------------------------- /src/components/sheets/UserMenuSheet.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Pressable, ScrollView, View} from 'react-native'; 3 | import {observer} from 'mobx-react-lite'; 4 | 5 | import MaterialCommunityIcon from 'react-native-vector-icons/MaterialCommunityIcons'; 6 | import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; 7 | 8 | import {User} from 'revolt.js'; 9 | 10 | import {app} from '../../Generic'; 11 | import {currentTheme, styles} from '../../Theme'; 12 | import {ContextButton, CopyIDButton, Text} from '../common/atoms'; 13 | 14 | export const UserMenuSheet = observer( 15 | ({state, user}: {state: any; user: User}) => { 16 | return ( 17 | <> 18 | 19 | { 26 | state(false); 27 | }}> 28 | 33 | 39 | Close 40 | 41 | 42 | {app.settings.get('ui.showDeveloperFeatures') ? ( 43 | 44 | ) : null} 45 | {user.relationship !== 'User' ? ( 46 | { 48 | app.openReportMenu(user, 'User'); 49 | state(false); 50 | }}> 51 | 52 | 57 | 58 | Report User 59 | 60 | ) : null} 61 | 62 | 63 | 64 | ); 65 | }, 66 | ); 67 | -------------------------------------------------------------------------------- /src/components/sheets/index.tsx: -------------------------------------------------------------------------------- 1 | export {ChannelInfoSheet} from './ChannelInfoSheet'; 2 | export {MemberListSheet} from './MemberListSheet'; 3 | export {MessageMenuSheet} from './MessageMenuSheet'; 4 | export {ProfileSheet} from './ProfileSheet'; 5 | export {ReportModal as ReportSheet} from './ReportSheet'; 6 | export {ServerInfoSheet} from './ServerInfoSheet'; 7 | export {SettingsSheet} from './SettingsSheet'; 8 | export {UserMenuSheet} from './UserMenuSheet'; 9 | -------------------------------------------------------------------------------- /src/components/views/ChannelView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {TouchableOpacity, View} from 'react-native'; 3 | import {observer} from 'mobx-react-lite'; 4 | 5 | import {ErrorBoundary} from 'react-error-boundary'; 6 | import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; 7 | 8 | import {Channel} from 'revolt.js'; 9 | 10 | import {app, ChannelIcon, client} from '../../Generic'; 11 | import {Messages, NewMessageView} from '../../MessageView'; 12 | import {MessageBox} from '../../MessageBox'; 13 | import {currentTheme, styles} from '../../Theme'; 14 | import {FriendsPage} from '../pages/FriendsPage'; 15 | import {HomePage} from '../pages/HomePage'; 16 | import {ChannelHeader} from '../navigation/ChannelHeader'; 17 | import {Button, Text} from '../common/atoms'; 18 | 19 | function MessageViewErrorMessage({ 20 | error, 21 | resetErrorBoundary, 22 | }: { 23 | error: any; 24 | resetErrorBoundary: Function; 25 | }) { 26 | console.error(`[MESSAGEVIEW] Uncaught error: ${error}`); 27 | return ( 28 | <> 29 | Error rendering messages: {error} 30 | 36 | 37 | ); 38 | } 39 | 40 | type CVChannel = Channel | 'friends' | 'debug' | null; 41 | 42 | export const ChannelView = observer( 43 | ({state, channel}: {state: any; channel: CVChannel}) => { 44 | const [replacementChannel, setReplacementChannel] = React.useState( 45 | undefined as Channel | undefined, 46 | ); 47 | const handledMessages = [] as string[]; 48 | React.useEffect(() => { 49 | async function getChannel() { 50 | const type = 'spam'; 51 | const c = 52 | type === 'spam' 53 | ? client.channels.get('01FBCT2PXQC0SSH6GD3V1BEHRP') 54 | : type === 'lounge' 55 | ? client.channels.get('01F7ZSBSFHCAAJQ92ZGTY67HMN') 56 | : await client.user?.openDM(); 57 | setReplacementChannel(c); 58 | } 59 | getChannel(); 60 | }); 61 | console.log( 62 | `[CHANNELVIEW] Rendering channel view for ${ 63 | channel instanceof Channel ? channel._id : channel 64 | }...`, 65 | ); 66 | return ( 67 | 68 | {channel ? ( 69 | channel === 'friends' ? ( 70 | 71 | ) : channel === 'debug' ? ( 72 | 73 | 74 | 75 | 76 | 77 | 78 | Debug Menu (New MessageView) 79 | 80 | 81 | 85 | 86 | ) : ( 87 | 88 | 89 | 90 | 100 | 101 | 102 | {channel.channel_type === 'DirectMessage' 103 | ? channel.recipient?.username 104 | : channel.channel_type === 'SavedMessages' 105 | ? 'Saved Notes' 106 | : channel.name} 107 | 108 | {channel.channel_type === 'Group' || channel.server ? ( 109 | 110 | app.openChannelContextMenu(channel)}> 112 | 117 | 118 | 119 | ) : null} 120 | {channel.channel_type === 'Group' ? ( 121 | 122 | 124 | app.openMemberList( 125 | channel, 126 | await channel.fetchMembers(), 127 | ) 128 | }> 129 | 134 | 135 | 136 | ) : null} 137 | 138 | {channel?.channel_type === 'VoiceChannel' ? ( 139 | 146 | 147 | Voice channels aren't supported in RVMob yet! 148 | 149 | 150 | In the meantime, you can join them via the web app or Revolt 151 | Desktop. 152 | 153 | 154 | ) : !channel?.nsfw || 155 | app.settings.get('ui.messaging.showNSFWContent') ? ( 156 | 157 | { 160 | app.openMessage(m); 161 | }} 162 | onUserPress={m => { 163 | app.openProfile(m.author, channel.server); 164 | }} 165 | onImagePress={a => { 166 | state.setState({imageViewerImage: a}); 167 | }} 168 | onUsernamePress={m => 169 | state.setState({ 170 | currentText: 171 | state.state.currentText + '<@' + m.author?._id + '>', 172 | }) 173 | } 174 | /> 175 | 176 | 177 | ) : ( 178 | 185 | 186 | Hold it! 187 | 188 | 189 | This is an NSFW channel. Are you sure you want to enter? 190 | {'\n'} 191 | (This can be reversed in Settings.) 192 | 193 | 202 | 203 | )} 204 | 205 | ) 206 | ) : ( 207 | 208 | )} 209 | 210 | ); 211 | }, 212 | ); 213 | -------------------------------------------------------------------------------- /src/lib/consts.ts: -------------------------------------------------------------------------------- 1 | // notable user IDs (mostly used for badges) 2 | export const USER_IDS = { 3 | // bots/built-in users 4 | automod: '01FHGJ3NPP7XANQQH8C2BE44ZY', 5 | platformModeration: '01FC17E1WTM2BGE4F3ARN3FDAF', 6 | 7 | // devs/team members 8 | developers: ['01FC1HP5H22F0M34MFFM9DZ099', '01FEEFJCKY5C4DMMJYZ20ACWWC'], 9 | teamMembers: { 10 | insert: '01EX2NCWQ0CHS3QJF0FEQS1GR4', 11 | lea: '01EXAF3KX65608AJ4NG27YG1HM', 12 | }, 13 | }; 14 | 15 | // notable servers (used for the home screen + other features) 16 | export const SPECIAL_SERVERS = { 17 | // the Revolt Lounge 18 | lounge: { 19 | id: '01F7ZSBSFHQ8TA81725KQCSDDP', 20 | invite: 'Testers', 21 | }, 22 | 23 | // RVMob's support server 24 | supportServer: { 25 | id: '01FKES1VJN27SVV4QJX82ZS3ME', 26 | invite: 'ZFGGw6ry', 27 | }, 28 | }; 29 | 30 | // default API URL - when support for other instances is added, this will be the default one 31 | export const DEFAULT_API_URL = 'https://api.divolt.xyz'; // TODO: switch to https://revolt.chat/api when it's available 32 | 33 | // default max side param - used to specify the size of images 34 | export const DEFAULT_MAX_SIDE = '128'; 35 | 36 | // default amount of messages to load 37 | export const DEFAULT_MESSAGE_LOAD_COUNT = 50; 38 | 39 | // server invite paths for the official instance 40 | export const INVITE_PATHS = [ 41 | 'app.revolt.chat/invite', 42 | 'nightly.revolt.chat/invite', 43 | 'local.revolt.chat/invite', 44 | 'rvlt.gg', 45 | 'divolt.xyz/invite', 46 | ]; 47 | 48 | // regex to find server invites 49 | export const RE_INVITE = new RegExp( 50 | `(?:${INVITE_PATHS.map(x => x?.split('.').join('\\.')).join( 51 | '|', 52 | )})/([A-Za-z0-9]*)`, 53 | 'g', 54 | ); 55 | 56 | // bot invite paths for the official instance 57 | export const BOT_INVITE_PATHS = [ 58 | 'app.revolt.chat/bot', 59 | 'nightly.revolt.chat/bot', 60 | 'local.revolt.chat/bot', 61 | 'divolt.xyz/bot', 62 | ]; 63 | 64 | // regex to find bot invites 65 | export const RE_BOT_INVITE = new RegExp( 66 | `(?:${BOT_INVITE_PATHS.map(x => x.split('.').join('\\.')).join( 67 | '|', 68 | )})/([A-Za-z0-9]*)`, 69 | 'g', 70 | ); 71 | 72 | // link to discover, used by the invite finder to ignore these links 73 | export const DISCOVER_URL = 'rvlt.gg/discover'; 74 | 75 | // link to the revolt wiki, per above 76 | export const WIKI_URL = 'wiki.rvlt.gg'; 77 | 78 | export type SpecialDateObject = { 79 | name: string; 80 | key: string; 81 | emoji: string; 82 | link: string; 83 | }; 84 | 85 | // list of holidays, used for the home screen 86 | export const SPECIAL_DATES = [ 87 | '1/1', 88 | '14/2', 89 | '31/3', 90 | '1/4', 91 | '31/10', 92 | '20/11', 93 | '31/12', 94 | 'month6', 95 | 'month12', 96 | ]; // NYD, Valentine's Day, TDOV, April Fool's, Halloween, TDOR and NYE + Pride Month/December 97 | 98 | // objects for each holiday 99 | export const SPECIAL_DATE_OBJECTS = { 100 | '1/1': { 101 | name: "New Year's Day", 102 | key: 'app-home-holiday-nyd', 103 | emoji: '🎉', 104 | link: "https://en.wikipedia.org/wiki/New_Year's_Eve", 105 | } as SpecialDateObject, 106 | '14/2': { 107 | name: "Valentine's Day", 108 | key: 'app-home-holiday-valentines', 109 | emoji: '💖', 110 | link: "https://en.wikipedia.org/wiki/Valentine's_Day", 111 | } as SpecialDateObject, 112 | '31/3': { 113 | name: 'International Trans Day of Visibility', 114 | key: 'app-home-holiday-tdov', 115 | emoji: '🏳️‍⚧️', 116 | link: 'https://en.wikipedia.org/wiki/TDOV', 117 | } as SpecialDateObject, 118 | '1/4': { 119 | name: "April Fools' Day", 120 | key: 'app-home-holiday-april-fools', 121 | emoji: '🤡', 122 | link: 'https://en.wikipedia.org/wiki/April_Fools%27_Day', 123 | } as SpecialDateObject, 124 | '31/10': { 125 | name: 'Halloween', 126 | key: 'app-home-holiday-halloween', 127 | emoji: '🎃', 128 | link: 'https://en.wikipedia.org/wiki/Halloween', 129 | } as SpecialDateObject, 130 | '20/11': { 131 | name: 'Trans Day of Remembrance', 132 | key: 'app-home-holiday-tdor', 133 | emoji: '🕯️', 134 | link: 'https://en.wikipedia.org/wiki/TDoR', 135 | } as SpecialDateObject, 136 | '31/12': { 137 | name: "New Year's Eve", 138 | key: 'app-home-holiday-nye', 139 | emoji: '⏰', 140 | link: "https://en.wikipedia.org/wiki/New_Year's_Eve", 141 | } as SpecialDateObject, 142 | month6: { 143 | name: 'Pride Month', 144 | key: 'app-home-holiday-pride', 145 | emoji: '🏳️‍🌈🏳️‍⚧️', 146 | link: 'https://en.wikipedia.org/wiki/Pride_Month', 147 | } as SpecialDateObject, 148 | month12: { 149 | name: 'Holiday Season', 150 | key: 'app-home-holiday-dechols', 151 | emoji: '🎄❄️', 152 | link: 'https://en.wikipedia.org/wiki/Christmas_and_holiday_season', 153 | } as SpecialDateObject, 154 | }; 155 | 156 | export const BADGES = { 157 | Developer: 1, 158 | Translator: 2, 159 | Supporter: 4, 160 | ResponsibleDisclosure: 8, 161 | Founder: 16, 162 | PlatformModeration: 32, 163 | ActiveSupporter: 64, 164 | Paw: 128, 165 | EarlyAdopter: 256, 166 | ReservedRelevantJokeBadge1: 512, 167 | ReservedRelevantJokeBadge2: 1024, 168 | }; 169 | 170 | export const LOADING_SCREEN_REMARKS = [ 171 | "I'm writing a complaint to the Head of Loading Screens.", 172 | "I don't think we can load any longer!", 173 | 'Fun fact: RVMob is built with React Native.', 174 | 'Better grab a book or something.', 175 | 'When will the madness end?', 176 | 'You know, what does DVMob even stand for?', 177 | 'Why do they call it a "building" if it\'s already built?', 178 | 'balls', 179 | ]; 180 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import {differenceInMinutes} from 'date-fns'; 2 | import {Channel, Message} from 'revolt.js'; 3 | import {decodeTime} from 'ulid'; 4 | 5 | import {client} from '../Generic'; 6 | import {currentTheme} from '../Theme'; 7 | import {DEFAULT_MESSAGE_LOAD_COUNT} from './consts'; 8 | 9 | /** 10 | * Returns the correct colour as a HEX string/theme variable. Supports regular HEX colours, client variables (e.g. `var(--accent)`) 11 | * and gradient roles (which, for now, will return as the first colour from the gradient - this will change if React Native adds support for gradients). 12 | * @param c The string to check over 13 | */ 14 | export function getColour(c: string) { 15 | // first check for css variables... 16 | const isVariable = c.match('var'); 17 | if (isVariable) { 18 | switch (c) { 19 | case 'var(--error)': 20 | return currentTheme.error; 21 | case 'var(--accent)': 22 | return currentTheme.accentColorForeground; 23 | case 'var(--foreground)': 24 | return currentTheme.foregroundPrimary; 25 | case 'var(--background)': 26 | return currentTheme.backgroundPrimary; 27 | default: 28 | break; 29 | } 30 | } 31 | 32 | // ...then check for gradients 33 | const gradientRegex = /(linear|conical|radial)-gradient\s*\(/; 34 | const degRegex = /[0-9]{0,3}deg,\s*/; 35 | const bracketRegex = /\)\s*(text)?$/; 36 | const percentRegex = /[0-9]{0,3}%(,|\))?\s*/; 37 | 38 | const isGradient = c.match(gradientRegex); 39 | if (isGradient) { 40 | const filteredC = c 41 | .replace(gradientRegex, '') 42 | .replace(bracketRegex, '') 43 | .replace(degRegex, '') 44 | .replace(percentRegex, ''); 45 | 46 | const filteredAsArray = filteredC.split(','); 47 | 48 | console.log( 49 | `[UTILS] getColour detected a gradient role: ${c}, filtered: ${filteredC}, to array: ${filteredAsArray}, ${filteredAsArray[0]}`, 50 | ); 51 | 52 | if (c.match('linear')) { 53 | return filteredAsArray[0]; 54 | } 55 | 56 | return c; 57 | } 58 | 59 | // at this point, c is probably just a regular HEX code so return it directly 60 | return c; 61 | } 62 | 63 | /** 64 | * Sleep for the specified amount of milliseconds before continuing. 65 | * @param ms The amount of time to sleep for in milliseconds 66 | */ 67 | export const sleep = (ms: number | undefined) => 68 | new Promise((r: any) => setTimeout(r, ms)); 69 | 70 | /** 71 | * Parses the given string for pings, channel links and custom emoji 72 | * @param text The text to parse 73 | * @returns The parsed text 74 | */ 75 | export function parseRevoltNodes(text: string) { 76 | text = text.replace(/<@[0-9A-Z]*>/g, ping => { 77 | let id = ping.slice(2, -1); 78 | let user = client.users.get(id); 79 | if (user) { 80 | return `[@${user.username}](/@${user._id})`; 81 | } 82 | return ping; 83 | }); 84 | text = text.replace(/<#[0-9A-Z]*>/g, ping => { 85 | let id = ping.slice(2, -1); 86 | let channel = client.channels.get(id); 87 | if (channel) { 88 | return `[#${channel.name 89 | ?.split(']') 90 | .join('\\]') 91 | .split('[') 92 | .join('\\[') 93 | .split('*') 94 | .join('\\*') 95 | .split('`') 96 | .join('\\`')}](/server/${channel.server?._id}/channel/${channel._id})`; 97 | } 98 | return ping; 99 | }); 100 | text = text.replace(/:[0-9A-Z]*:/g, ping => { 101 | let id = ping.slice(1, -1); 102 | let emoji = client.emojis.get(id); 103 | if (emoji) { 104 | return `%EMOJI - [${emoji.name}](${emoji.imageURL}) - EMOJI%`; 105 | } 106 | return ping; 107 | }); 108 | return text; 109 | } 110 | 111 | export function getReadableFileSize(size: number | null) { 112 | return size !== null 113 | ? size / 1000000 >= 0.01 114 | ? `${(size / 1000000).toFixed(2)} MB` 115 | : size / 10000 >= 0.01 116 | ? `${(size / 1000).toFixed(2)} KB` 117 | : `${size} bytes` 118 | : 'Unknown'; 119 | } 120 | 121 | export function calculateGrouped(msg1: Message, msg2: Message) { 122 | // if the author is somehow null don't group the message 123 | if (!msg1.author || !msg2.author) { 124 | return false; 125 | } 126 | return ( 127 | // a message is grouped with the previous message if all of the following is true: 128 | msg1.author._id === msg2.author._id && // the author is the same 129 | !(msg2.reply_ids && msg2.reply_ids.length > 0) && // the message is not a reply 130 | differenceInMinutes( 131 | // the time difference is less than 7 minutes and 132 | decodeTime(msg1._id), 133 | decodeTime(msg2._id), 134 | ) < 7 && 135 | (msg2.masquerade // the masquerade is the same 136 | ? msg2.masquerade.avatar === msg1.masquerade?.avatar && 137 | msg2.masquerade.name === msg1.masquerade?.name 138 | : true) 139 | ); 140 | } 141 | 142 | type FetchInput = { 143 | id?: string; 144 | type?: 'before' | 'after'; 145 | }; 146 | 147 | export async function fetchMessages( 148 | channel: Channel, 149 | input: FetchInput, 150 | existingMessages: Message[], 151 | sliceMessages?: false, 152 | ) { 153 | const type = input.type ?? 'before'; 154 | let params = { 155 | // input.before ? DEFAULT_MESSAGE_LOAD_COUNT / 2 : 156 | limit: DEFAULT_MESSAGE_LOAD_COUNT, 157 | } as {limit: number; before?: string; after?: string}; 158 | params[type] = input.id; 159 | // if (type == "after") { 160 | // params.sort = "Oldest" 161 | // } 162 | const res = await channel.fetchMessagesWithUsers(params); 163 | console.log( 164 | `[FETCHMESSAGES] Finished fetching ${res.messages.length} message(s) for ${channel._id}`, 165 | ); 166 | 167 | let oldMessages = existingMessages; 168 | if (sliceMessages) { 169 | if (input.type === 'before') { 170 | oldMessages = oldMessages.slice(0, DEFAULT_MESSAGE_LOAD_COUNT / 2 - 1); 171 | } else if (input.type === 'after') { 172 | oldMessages = oldMessages.slice( 173 | DEFAULT_MESSAGE_LOAD_COUNT / 2 - 1, 174 | DEFAULT_MESSAGE_LOAD_COUNT - 1, 175 | ); 176 | } 177 | } 178 | let messages = res.messages.reverse(); 179 | let result = 180 | input.type === 'before' 181 | ? messages.concat(oldMessages) 182 | : input.type === 'after' 183 | ? oldMessages.concat(messages) 184 | : messages; 185 | console.log( 186 | `[FETCHEDMESSAGES] Finished preparing fetched messages for ${channel._id}`, 187 | ); 188 | 189 | return result; 190 | } 191 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true 4 | }, 5 | "extends": "@tsconfig/react-native/tsconfig.json" 6 | } --------------------------------------------------------------------------------