├── .editorconfig ├── .github ├── assets │ ├── chat.png │ ├── drawer.png │ ├── themed_chat.png │ └── themed_drawer.png └── workflows │ ├── android.yml │ └── crowdin.yml ├── .gitignore ├── LICENSE ├── README.md ├── STATUS.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── xinto │ │ └── opencord │ │ ├── OpenCord.kt │ │ ├── ast │ │ ├── node │ │ │ ├── ChannelMentionNode.kt │ │ │ ├── EmoteNode.kt │ │ │ ├── SpoilerNode.kt │ │ │ ├── StyledNode.kt │ │ │ ├── TextNode.kt │ │ │ └── UserMentionNode.kt │ │ ├── rule │ │ │ ├── DiscordRules.kt │ │ │ ├── MarkdownRules.kt │ │ │ └── OtherRules.kt │ │ └── util │ │ │ ├── Patterns.kt │ │ │ └── Rules.kt │ │ ├── db │ │ ├── Converters.kt │ │ ├── dao │ │ │ ├── AccountsDao.kt │ │ │ ├── AttachmentsDao.kt │ │ │ ├── ChannelsDao.kt │ │ │ ├── EmbedsDao.kt │ │ │ ├── GuildsDao.kt │ │ │ ├── MessagesDao.kt │ │ │ ├── ReactionsDao.kt │ │ │ ├── UnreadStatesDao.kt │ │ │ └── UsersDao.kt │ │ ├── database │ │ │ ├── AccountDatabase.kt │ │ │ └── CacheDatabase.kt │ │ └── entity │ │ │ ├── EntityAccount.kt │ │ │ ├── channel │ │ │ ├── EntityChannel.kt │ │ │ └── EntityUnreadState.kt │ │ │ ├── guild │ │ │ └── EntityGuild.kt │ │ │ ├── message │ │ │ ├── EntityAttachment.kt │ │ │ ├── EntityEmbed.kt │ │ │ └── EntityMessage.kt │ │ │ ├── reactions │ │ │ └── EntityReaction.kt │ │ │ └── user │ │ │ └── EntityUser.kt │ │ ├── di │ │ ├── DatabaseModule.kt │ │ ├── GatewayModule.kt │ │ ├── HCaptcha.kt │ │ ├── HttpModule.kt │ │ ├── LoggerModule.kt │ │ ├── ManagerModule.kt │ │ ├── ProviderModule.kt │ │ ├── ServiceModule.kt │ │ ├── SimpleASTModule.kt │ │ ├── StoreModule.kt │ │ └── ViewModelModule.kt │ │ ├── domain │ │ ├── Mentionable.kt │ │ ├── activity │ │ │ ├── ActivityType.kt │ │ │ ├── DomainActivity.kt │ │ │ ├── DomainActivityAssets.kt │ │ │ ├── DomainActivityButton.kt │ │ │ ├── DomainActivityMetadata.kt │ │ │ ├── DomainActivityParty.kt │ │ │ ├── DomainActivitySecrets.kt │ │ │ ├── DomainActivityTimestamp.kt │ │ │ └── types │ │ │ │ ├── DomainActivityCustom.kt │ │ │ │ ├── DomainActivityGame.kt │ │ │ │ ├── DomainActivityListening.kt │ │ │ │ ├── DomainActivityStreaming.kt │ │ │ │ └── DomainActivityUnknown.kt │ │ ├── attachment │ │ │ ├── DomainAttachment.kt │ │ │ ├── DomainFileAttachment.kt │ │ │ ├── DomainPictureAttachment.kt │ │ │ └── DomainVideoAttachment.kt │ │ ├── channel │ │ │ ├── DomainAnnouncementChannel.kt │ │ │ ├── DomainCategoryChannel.kt │ │ │ ├── DomainChannel.kt │ │ │ ├── DomainTextChannel.kt │ │ │ ├── DomainUnreadState.kt │ │ │ └── DomainVoiceChannel.kt │ │ ├── embed │ │ │ ├── DomainEmbed.kt │ │ │ └── DomainEmbedFooter.kt │ │ ├── emoji │ │ │ ├── DomainEmoji.kt │ │ │ ├── DomainGuildEmoji.kt │ │ │ ├── DomainUnicodeEmoji.kt │ │ │ └── DomainUnknownEmoji.kt │ │ ├── guild │ │ │ └── DomainGuild.kt │ │ ├── login │ │ │ └── DomainLogin.kt │ │ ├── member │ │ │ └── DomainGuildMember.kt │ │ ├── message │ │ │ ├── DomainMessage.kt │ │ │ ├── DomainMessageMemberJoin.kt │ │ │ ├── DomainMessageRegular.kt │ │ │ ├── DomainMessageUnknown.kt │ │ │ └── reaction │ │ │ │ └── DomainReaction.kt │ │ ├── permissions │ │ │ └── DomainPermission.kt │ │ ├── user │ │ │ ├── DomainUser.kt │ │ │ ├── DomainUserPrivate.kt │ │ │ ├── DomainUserPrivateReady.kt │ │ │ └── DomainUserPublic.kt │ │ └── usersettings │ │ │ ├── DomainCustomStatus.kt │ │ │ ├── DomainFriendSources.kt │ │ │ ├── DomainGuildFolder.kt │ │ │ ├── DomainThemeSetting.kt │ │ │ ├── DomainUserSettings.kt │ │ │ └── DomainUserStatus.kt │ │ ├── gateway │ │ ├── CloseCode.kt │ │ ├── DiscordGateway.kt │ │ ├── dto │ │ │ ├── GuildDeleteData.kt │ │ │ ├── Heartbeat.kt │ │ │ ├── Identification.kt │ │ │ ├── MessageAckData.kt │ │ │ ├── MessageDeleteData.kt │ │ │ ├── Presence.kt │ │ │ ├── Reaction.kt │ │ │ ├── Ready.kt │ │ │ ├── RequestGuildMembers.kt │ │ │ ├── Resume.kt │ │ │ └── SessionsReplaceData.kt │ │ ├── event │ │ │ ├── Channel.kt │ │ │ ├── Event.kt │ │ │ ├── Guild.kt │ │ │ ├── GuildMemberChunk.kt │ │ │ ├── Message.kt │ │ │ ├── Reaction.kt │ │ │ ├── Ready.kt │ │ │ ├── Sessions.kt │ │ │ ├── UserSettings.kt │ │ │ └── UserUpdateEvent.kt │ │ └── io │ │ │ ├── Capabilities.kt │ │ │ ├── EventName.kt │ │ │ ├── OpCode.kt │ │ │ └── Payload.kt │ │ ├── manager │ │ ├── AccountCookieManager.kt │ │ ├── AccountManager.kt │ │ ├── ActivityManager.kt │ │ ├── ClipboardManager.kt │ │ ├── DownloadManager.kt │ │ ├── PersistentDataManager.kt │ │ ├── ToastManager.kt │ │ └── base │ │ │ └── BasePreferenceManager.kt │ │ ├── provider │ │ ├── PropertyProvider.kt │ │ └── TelemetryProvider.kt │ │ ├── rest │ │ ├── body │ │ │ ├── Login.kt │ │ │ ├── Message.kt │ │ │ ├── TwoFactor.kt │ │ │ └── XSuperProperties.kt │ │ ├── models │ │ │ ├── ApiAttachment.kt │ │ │ ├── ApiChannel.kt │ │ │ ├── ApiColor.kt │ │ │ ├── ApiExperiments.kt │ │ │ ├── ApiGuild.kt │ │ │ ├── ApiGuildMemberChunk.kt │ │ │ ├── ApiPermissions.kt │ │ │ ├── GuildMember.kt │ │ │ ├── Snowflake.kt │ │ │ ├── activity │ │ │ │ ├── ApiActivity.kt │ │ │ │ ├── ApiActivityAssets.kt │ │ │ │ ├── ApiActivityButton.kt │ │ │ │ ├── ApiActivityMetadata.kt │ │ │ │ ├── ApiActivityParty.kt │ │ │ │ ├── ApiActivitySecrets.kt │ │ │ │ └── ApiActivityTimestamp.kt │ │ │ ├── embed │ │ │ │ ├── ApiEmbed.kt │ │ │ │ ├── ApiEmbedAuthor.kt │ │ │ │ ├── ApiEmbedField.kt │ │ │ │ ├── ApiEmbedFooter.kt │ │ │ │ ├── ApiEmbedMedia.kt │ │ │ │ └── ApiEmbedProvider.kt │ │ │ ├── emoji │ │ │ │ └── ApiEmoji.kt │ │ │ ├── login │ │ │ │ ├── ApiLogin.kt │ │ │ │ └── ApiLoginUserSettings.kt │ │ │ ├── message │ │ │ │ ├── ApiMessage.kt │ │ │ │ └── ApiMessageType.kt │ │ │ ├── reaction │ │ │ │ └── ApiReaction.kt │ │ │ └── user │ │ │ │ ├── ApiUser.kt │ │ │ │ └── settings │ │ │ │ ├── ApiCustomStatus.kt │ │ │ │ ├── ApiFriendSources.kt │ │ │ │ ├── ApiGuildFolder.kt │ │ │ │ └── ApiUserSettings.kt │ │ └── service │ │ │ ├── DiscordApiService.kt │ │ │ ├── DiscordAuthService.kt │ │ │ └── DiscordCdnService.kt │ │ ├── store │ │ ├── ChannelStore.kt │ │ ├── CurrentUserStore.kt │ │ ├── Event.kt │ │ ├── GuildStore.kt │ │ ├── LastMessageStore.kt │ │ ├── MessageStore.kt │ │ ├── ReactionStore.kt │ │ ├── SessionStore.kt │ │ ├── UnreadStore.kt │ │ └── UserSettingsStore.kt │ │ ├── ui │ │ ├── AppActivity.kt │ │ ├── LoginActivity.kt │ │ ├── SplashScreenActivity.kt │ │ ├── components │ │ │ ├── OCBadgeBox.kt │ │ │ ├── OCImage.kt │ │ │ ├── OCTextField.kt │ │ │ ├── attachment │ │ │ │ ├── AttachmentPicture.kt │ │ │ │ └── AttachmentVideo.kt │ │ │ ├── captcha │ │ │ │ └── HCaptcha.kt │ │ │ ├── channel │ │ │ │ └── list │ │ │ │ │ ├── ChannelListCategoryItem.kt │ │ │ │ │ └── ChannelListRegularItem.kt │ │ │ ├── embed │ │ │ │ ├── Embed.kt │ │ │ │ ├── EmbedAuthor.kt │ │ │ │ ├── EmbedField.kt │ │ │ │ ├── EmbedFooter.kt │ │ │ │ ├── EmbedVideo.kt │ │ │ │ └── SpotifyEmbed.kt │ │ │ ├── guild │ │ │ │ └── list │ │ │ │ │ ├── GuildsListHeaderItem.kt │ │ │ │ │ ├── GuildsListItemImage.kt │ │ │ │ │ ├── GuildsListRegularItem.kt │ │ │ │ │ └── GuildsListTextItem.kt │ │ │ ├── indicator │ │ │ │ ├── MentionCountBadge.kt │ │ │ │ ├── UnreadIndicator.kt │ │ │ │ └── UserStatusIcon.kt │ │ │ └── message │ │ │ │ ├── MessageAuthor.kt │ │ │ │ ├── MessageAvatar.kt │ │ │ │ ├── MessageContent.kt │ │ │ │ ├── MessageReaction.kt │ │ │ │ ├── MessageRegular.kt │ │ │ │ └── reply │ │ │ │ ├── MessageReferenced.kt │ │ │ │ ├── MessageReferencedAuthor.kt │ │ │ │ ├── MessageReferencedContent.kt │ │ │ │ └── MessageReplyBranch.kt │ │ ├── navigation │ │ │ ├── AppDestination.kt │ │ │ └── LoginDestination.kt │ │ ├── screens │ │ │ ├── SettingsScreen.kt │ │ │ ├── home │ │ │ │ ├── HomeScreen.kt │ │ │ │ └── panels │ │ │ │ │ ├── HomeNavButtons.kt │ │ │ │ │ ├── channel │ │ │ │ │ ├── ChannelsList.kt │ │ │ │ │ ├── ChannelsListLoaded.kt │ │ │ │ │ ├── ChannelsListLoading.kt │ │ │ │ │ └── ChannelsListUnselected.kt │ │ │ │ │ ├── chat │ │ │ │ │ ├── Chat.kt │ │ │ │ │ ├── ChatError.kt │ │ │ │ │ ├── ChatLoaded.kt │ │ │ │ │ ├── ChatLoading.kt │ │ │ │ │ └── ChatUnselected.kt │ │ │ │ │ ├── chatinput │ │ │ │ │ └── ChatInput.kt │ │ │ │ │ ├── currentuser │ │ │ │ │ ├── CurrentUser.kt │ │ │ │ │ ├── CurrentUserContent.kt │ │ │ │ │ ├── CurrentUserLoaded.kt │ │ │ │ │ ├── CurrentUserLoading.kt │ │ │ │ │ └── sheet │ │ │ │ │ │ └── CurrentUserSheet.kt │ │ │ │ │ ├── guild │ │ │ │ │ ├── GuildsList.kt │ │ │ │ │ ├── GuildsListLoaded.kt │ │ │ │ │ └── GuildsListLoading.kt │ │ │ │ │ ├── member │ │ │ │ │ └── MembersList.kt │ │ │ │ │ └── messagemenu │ │ │ │ │ ├── MessageMenu.kt │ │ │ │ │ ├── MessageMenuLoaded.kt │ │ │ │ │ ├── MessageMenuLoading.kt │ │ │ │ │ └── MessageMenuPreviewMessage.kt │ │ │ ├── login │ │ │ │ ├── LoginLandingScreen.kt │ │ │ │ └── LoginScreen.kt │ │ │ ├── mentions │ │ │ │ └── MentionsScreen.kt │ │ │ └── pins │ │ │ │ ├── PinsScreen.kt │ │ │ │ ├── PinsScreenError.kt │ │ │ │ ├── PinsScreenLoaded.kt │ │ │ │ └── PinsScreenLoading.kt │ │ ├── theme │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ │ ├── util │ │ │ ├── Animation.kt │ │ │ ├── Composable.kt │ │ │ ├── CompositePaddingValues.kt │ │ │ ├── ContentAlpha.kt │ │ │ ├── CornerBasedShape.kt │ │ │ ├── GestureNavigation.kt │ │ │ ├── LocalViewModel.kt │ │ │ ├── MessageInlineContent.kt │ │ │ ├── NavUtil.kt │ │ │ ├── ScrollableWebView.kt │ │ │ ├── UnsafeImmutableList.kt │ │ │ └── VoidablePaddingValues.kt │ │ └── viewmodel │ │ │ ├── ChannelPinsViewModel.kt │ │ │ ├── ChannelsViewModel.kt │ │ │ ├── ChatInputViewModel.kt │ │ │ ├── ChatViewModel.kt │ │ │ ├── CurrentUserViewModel.kt │ │ │ ├── GuildsViewModel.kt │ │ │ ├── LoginViewModel.kt │ │ │ ├── MentionsViewModel.kt │ │ │ ├── MessageMenuViewModel.kt │ │ │ └── base │ │ │ └── BasePersistenceViewModel.kt │ │ └── util │ │ ├── DiscordLocale.kt │ │ ├── FlowUtil.kt │ │ ├── KtorUtil.kt │ │ ├── Logger.kt │ │ ├── NonceGenerator.kt │ │ ├── Quad.kt │ │ ├── StdUtil.kt │ │ ├── TextParser.kt │ │ ├── Throttling.kt │ │ └── Timestamp.kt │ └── res │ ├── drawable │ ├── ic_account_switch.xml │ ├── ic_add.xml │ ├── ic_add_reaction.xml │ ├── ic_arrow_back.xml │ ├── ic_at_sign.xml │ ├── ic_cancel.xml │ ├── ic_channel_announcements.xml │ ├── ic_delete.xml │ ├── ic_discord_logo.xml │ ├── ic_discord_logo_text.xml │ ├── ic_edit.xml │ ├── ic_error.xml │ ├── ic_file_copy.xml │ ├── ic_filter.xml │ ├── ic_group.xml │ ├── ic_guild_badge_premium_tier_1.xml │ ├── ic_guild_badge_premium_tier_1_banner.xml │ ├── ic_guild_badge_premium_tier_2.xml │ ├── ic_guild_badge_premium_tier_2_banner.xml │ ├── ic_guild_badge_premium_tier_3.xml │ ├── ic_guild_badge_premium_tier_3_banner.xml │ ├── ic_keyboard_arrow_down.xml │ ├── ic_launcher_foreground.xml │ ├── ic_launcher_foreground_no_shadow.xml │ ├── ic_link.xml │ ├── ic_mark_unread.xml │ ├── ic_menu.xml │ ├── ic_people.xml │ ├── ic_pin.xml │ ├── ic_reply.xml │ ├── ic_search.xml │ ├── ic_set_custom_status.xml │ ├── ic_settings.xml │ ├── ic_status_dnd.xml │ ├── ic_status_idle.xml │ ├── ic_status_invisible.xml │ ├── ic_status_online.xml │ ├── ic_status_streaming.xml │ ├── ic_tag.xml │ └── ic_volume_up.xml │ ├── font │ ├── inter_black.ttf │ ├── inter_bold.ttf │ ├── inter_extrabold.ttf │ ├── inter_extralight.ttf │ ├── inter_light.ttf │ ├── inter_medium.ttf │ ├── inter_regular.ttf │ ├── inter_semibold.ttf │ └── inter_thin.ttf │ ├── 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 │ ├── raw │ └── keep.xml │ ├── values-af-rZA │ └── strings.xml │ ├── values-ar-rSA │ └── strings.xml │ ├── values-ca-rES │ └── strings.xml │ ├── values-cs-rCZ │ └── strings.xml │ ├── values-da-rDK │ └── strings.xml │ ├── values-de-rDE │ └── strings.xml │ ├── values-el-rGR │ └── strings.xml │ ├── values-es-rES │ └── strings.xml │ ├── values-fi-rFI │ └── strings.xml │ ├── values-fr-rFR │ └── strings.xml │ ├── values-hi-rIN │ └── strings.xml │ ├── values-hu-rHU │ └── strings.xml │ ├── values-in-rID │ └── strings.xml │ ├── values-it-rIT │ └── strings.xml │ ├── values-iw-rIL │ └── strings.xml │ ├── values-ja-rJP │ └── strings.xml │ ├── values-ko-rKR │ └── strings.xml │ ├── values-nl-rNL │ └── strings.xml │ ├── values-no-rNO │ └── strings.xml │ ├── values-pl-rPL │ └── strings.xml │ ├── values-pt-rBR │ └── strings.xml │ ├── values-pt-rPT │ └── strings.xml │ ├── values-ro-rRO │ └── strings.xml │ ├── values-ru-rRU │ └── strings.xml │ ├── values-sr-rSP │ └── strings.xml │ ├── values-sv-rSE │ └── strings.xml │ ├── values-tr-rTR │ └── strings.xml │ ├── values-uk-rUA │ └── strings.xml │ ├── values-vi-rVN │ └── strings.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values-zh-rTW │ └── strings.xml │ └── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml ├── build.gradle.kts ├── crowdin.yml ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── simpleast-compose ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml └── java └── com └── xinto └── simpleast ├── DSL.kt ├── Node.kt ├── ParseException.kt ├── ParseSpec.kt ├── Parser.kt ├── Renderer.kt └── Rule.kt /.github/assets/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/.github/assets/chat.png -------------------------------------------------------------------------------- /.github/assets/drawer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/.github/assets/drawer.png -------------------------------------------------------------------------------- /.github/assets/themed_chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/.github/assets/themed_chat.png -------------------------------------------------------------------------------- /.github/assets/themed_drawer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/.github/assets/themed_drawer.png -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Build APK 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | paths-ignore: 8 | - '**.md' 9 | pull_request: 10 | branches: 11 | - '*' 12 | paths-ignore: 13 | - '**.md' 14 | workflow_dispatch: 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | - name: Set up JDK 11 23 | uses: actions/setup-java@v2 24 | with: 25 | java-version: 11 26 | distribution: 'zulu' 27 | cache: 'gradle' 28 | 29 | - name: Build OpenCord 30 | run: ./gradlew app:assembleDebug 31 | 32 | - name: Upload Discord APK 33 | uses: actions/upload-artifact@v2 34 | with: 35 | name: opencord 36 | path: app/build/outputs/apk/debug/app-debug.apk 37 | -------------------------------------------------------------------------------- /.github/workflows/crowdin.yml: -------------------------------------------------------------------------------- 1 | name: Sync Crowdin 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | paths: 8 | - 'app/src/main/res/values/strings.xml' 9 | - '.github/workflows/crowdin.yml' 10 | schedule: 11 | - cron: "0 17 * * 6" # "At 17:00 on Saturday." 12 | workflow_dispatch: 13 | 14 | jobs: 15 | sync-crowdin: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | 21 | - name: Crowdin 22 | uses: crowdin/github-action@1.4.11 23 | with: 24 | config: crowdin.yml 25 | upload_translations: true 26 | download_translations: true 27 | push_translations: true 28 | create_pull_request: false 29 | localization_branch_name: l10n 30 | commit_message: 'chore(i18n): sync translations' 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_ID }} 34 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_TOKEN }} 35 | 36 | - name: Merge 37 | run: | 38 | sudo chmod -R ugo+rwX . 39 | git checkout master 40 | git add * 41 | git merge l10n 42 | git push 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .DS_Store 3 | .cxx 4 | .externalNativeBuild 5 | .gradle 6 | .idea 7 | /build 8 | /captures 9 | /local.properties 10 | *.log 11 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | /discord 4 | /fosscord -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 28 | 29 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/OpenCord.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord 2 | 3 | import android.app.Application 4 | import com.xinto.opencord.di.* 5 | import org.koin.android.ext.koin.androidContext 6 | import org.koin.core.context.startKoin 7 | 8 | class OpenCord : Application() { 9 | 10 | override fun onCreate() { 11 | super.onCreate() 12 | 13 | startKoin { 14 | androidContext(this@OpenCord) 15 | modules( 16 | gatewayModule, 17 | httpModule, 18 | managerModule, 19 | serviceModule, 20 | simpleAstModule, 21 | viewModelModule, 22 | loggerModule, 23 | providerModule, 24 | databaseModule, 25 | storeModule, 26 | hcaptchaModule, 27 | ) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ast/node/ChannelMentionNode.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ast.node 2 | 3 | import androidx.compose.ui.text.AnnotatedString 4 | import androidx.compose.ui.text.SpanStyle 5 | import androidx.compose.ui.text.font.FontWeight 6 | import androidx.compose.ui.text.withStyle 7 | import com.xinto.opencord.ui.theme.Blurple 8 | import com.xinto.simpleast.Node 9 | 10 | class ChannelMentionNode( 11 | val channel: String? 12 | ) : Node() { 13 | //@formatter:off 14 | context(AnnotatedString.Builder) 15 | override fun render(renderContext: RC) { //@formatter:on 16 | withStyle( 17 | SpanStyle( 18 | color = Blurple, 19 | background = Blurple.copy(alpha = 0.2f), 20 | fontWeight = FontWeight.SemiBold, 21 | ), 22 | ) { 23 | append("#$channel") 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ast/node/EmoteNode.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ast.node 2 | 3 | import androidx.compose.foundation.text.appendInlineContent 4 | import androidx.compose.ui.text.AnnotatedString 5 | import com.xinto.simpleast.Node 6 | 7 | class EmoteNode( 8 | val emoteId: String 9 | ) : Node() { 10 | //@formatter:off 11 | context(AnnotatedString.Builder) 12 | override fun render(renderContext: RC) { //@formatter:on 13 | appendInlineContent("emote", emoteId) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ast/node/SpoilerNode.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ast.node 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.text.AnnotatedString 5 | import androidx.compose.ui.text.SpanStyle 6 | import androidx.compose.ui.text.withStyle 7 | import com.xinto.simpleast.Node 8 | 9 | class SpoilerNode : Node.Parent() { 10 | //@formatter:off 11 | context(AnnotatedString.Builder) 12 | override fun render(renderContext: RC) { //@formatter:on 13 | withStyle( 14 | SpanStyle( 15 | background = Color.Black, 16 | ), 17 | ) { 18 | super.render(renderContext) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ast/node/StyledNode.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ast.node 2 | 3 | import androidx.compose.ui.text.AnnotatedString 4 | import androidx.compose.ui.text.SpanStyle 5 | import androidx.compose.ui.text.withStyle 6 | import com.xinto.simpleast.Node 7 | 8 | class StyledNode( 9 | val styles: Collection, 10 | ) : Node.Parent() { 11 | //@formatter:off 12 | context(AnnotatedString.Builder) 13 | override fun render(renderContext: RC) { //@formatter:on 14 | var style = SpanStyle() 15 | 16 | styles.forEach { 17 | style += it 18 | } 19 | 20 | withStyle(style) { 21 | super.render(renderContext) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ast/node/TextNode.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ast.node 2 | 3 | import androidx.compose.ui.text.AnnotatedString 4 | import com.xinto.simpleast.Node 5 | 6 | open class TextNode( 7 | val content: String, 8 | ) : Node() { 9 | //@formatter:off 10 | context(AnnotatedString.Builder) 11 | override fun render(renderContext: RC) { //@formatter:on 12 | append(content) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ast/node/UserMentionNode.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ast.node 2 | 3 | import androidx.compose.ui.text.AnnotatedString 4 | import androidx.compose.ui.text.SpanStyle 5 | import androidx.compose.ui.text.font.FontWeight 6 | import androidx.compose.ui.text.withStyle 7 | import com.xinto.opencord.ui.theme.Blurple 8 | import com.xinto.simpleast.Node 9 | 10 | class UserMentionNode( 11 | private val userId: String? 12 | ) : Node() { 13 | //@formatter:off 14 | context(AnnotatedString.Builder) 15 | override fun render(renderContext: RC) { //@formatter:on 16 | withStyle( 17 | SpanStyle( 18 | color = Blurple, 19 | background = Blurple.copy(alpha = 0.2f), 20 | fontWeight = FontWeight.SemiBold, 21 | ), 22 | ) { 23 | append("@$userId") 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ast/rule/MarkdownRules.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ast.rule 2 | 3 | import androidx.compose.ui.text.SpanStyle 4 | import androidx.compose.ui.text.font.FontStyle 5 | import androidx.compose.ui.text.font.FontWeight 6 | import androidx.compose.ui.text.style.TextDecoration 7 | import com.xinto.opencord.ast.util.* 8 | import com.xinto.simpleast.Node 9 | import com.xinto.simpleast.Rule 10 | 11 | fun createBoldTextRule() = 12 | createSimpleTextRule( 13 | pattern = PATTERN_BOLD_TEXT, 14 | styles = listOf( 15 | SpanStyle( 16 | fontWeight = FontWeight.Bold, 17 | ), 18 | ), 19 | ) 20 | 21 | fun createItalicAsteriskTextRule(): Rule, S> = 22 | createSimpleTextRule( 23 | pattern = PATTERN_ITALIC_ASTERISK_TEXT, 24 | styles = listOf(SpanStyle(fontStyle = FontStyle.Italic)), 25 | ) 26 | 27 | fun createItalicUnderscoreTextRule(): Rule, S> = 28 | createSimpleTextRule( 29 | pattern = PATTERN_ITALIC_UNDERSCORE_TEXT, 30 | styles = listOf(SpanStyle(fontStyle = FontStyle.Italic)), 31 | ) 32 | 33 | fun createUnderlineTextRule() = 34 | createSimpleTextRule( 35 | pattern = PATTERN_UNDERLINE, 36 | styles = listOf( 37 | SpanStyle(textDecoration = TextDecoration.Underline), 38 | ), 39 | ) 40 | 41 | fun createStrikeThroughRule() = 42 | createSimpleTextRule( 43 | pattern = PATTERN_STRIKETHROUGH, 44 | styles = listOf( 45 | SpanStyle(textDecoration = TextDecoration.LineThrough), 46 | ), 47 | ) 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ast/rule/OtherRules.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ast.rule 2 | 3 | import com.xinto.opencord.ast.node.TextNode 4 | import com.xinto.opencord.ast.util.PATTERN_OTHER 5 | import com.xinto.simpleast.Node 6 | import com.xinto.simpleast.ParseSpec 7 | import com.xinto.simpleast.Rule 8 | import com.xinto.simpleast.createRule 9 | 10 | fun createOtherRule(): Rule, S> = 11 | createRule(PATTERN_OTHER) { matcher, _, state -> 12 | ParseSpec.createTerminal( 13 | node = TextNode(matcher.group()), 14 | state = state, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ast/util/Patterns.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ast.util 2 | 3 | import java.util.regex.Pattern 4 | 5 | val PATTERN_OTHER: Pattern = 6 | Pattern.compile("""^[\s\S]+?(?=[^0-9A-Za-z\s\u00c0-\uffff]|\n| {2,}\n|\w+:\S|${'$'})""") 7 | val PATTERN_BOLD_TEXT: Pattern = Pattern.compile("""^\*\*([\s\S]+?)\*\*(?!\*)""") 8 | val PATTERN_ITALIC_ASTERISK_TEXT: Pattern = Pattern.compile("""^\*(?=\S)((?:\*\*|\s+(?:[^*\s]|\*\*)|[^\s*])+?)\*(?!\*)""") 9 | val PATTERN_ITALIC_UNDERSCORE_TEXT: Pattern = Pattern.compile("""^\b_((?:__|\\[\s\S]|[^\\_])+?)_\b""") 10 | val PATTERN_SPOILER: Pattern = Pattern.compile("""^\|\|([\s\S]+?)\|\|""") 11 | val PATTERN_STRIKETHROUGH: Pattern = Pattern.compile("""^~~(?=\S)([\s\S]+?\S)~~""") 12 | val PATTERN_ESCAPE: Pattern = Pattern.compile("""^\\([^0-9A-Za-z\s])""") 13 | val PATTERN_UNDERLINE: Pattern = Pattern.compile("""^__([\s\S]+?)__(?!_)""") 14 | val PATTERN_EMOTE: Pattern = Pattern.compile("""^<(a)?:([a-zA-Z_0-9]+):(\d+)>""") 15 | val PATTERN_USER_MENTION: Pattern = Pattern.compile("""^<@!?(\d+)>""") 16 | val PATTERN_EVERYONE_MENTION: Pattern = Pattern.compile("""^@(everyone|here)""") 17 | val PATTERN_CHANNEL_MENTION: Pattern = Pattern.compile("""^<#(\d+)>""") -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ast/util/Rules.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ast.util 2 | 3 | import androidx.compose.ui.text.SpanStyle 4 | import com.xinto.opencord.ast.node.StyledNode 5 | import com.xinto.simpleast.Node 6 | import com.xinto.simpleast.ParseSpec 7 | import com.xinto.simpleast.Rule 8 | import com.xinto.simpleast.createRule 9 | import java.util.regex.Pattern 10 | 11 | fun createSimpleTextRule( 12 | pattern: Pattern, 13 | styles: Collection 14 | ): Rule, S> = 15 | createRule(pattern) { matcher, _, state -> 16 | val node = StyledNode(styles) 17 | ParseSpec.createNonTerminal( 18 | node = node, 19 | state = state, 20 | startIndex = matcher.start(1), 21 | endIndex = matcher.end(1), 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/db/Converters.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.db 2 | 3 | import androidx.room.TypeConverter 4 | import com.xinto.opencord.rest.models.embed.ApiEmbedField 5 | import kotlinx.serialization.decodeFromString 6 | import kotlinx.serialization.encodeToString 7 | import kotlinx.serialization.json.Json 8 | 9 | @Suppress("unused") 10 | class Converters { 11 | @TypeConverter 12 | fun fromEmbedFields(fields: List?): String? { 13 | return Json.encodeToString(fields ?: return null) 14 | } 15 | 16 | @TypeConverter 17 | fun toEmbedFields(fields: String?): List? { 18 | return Json.decodeFromString(fields ?: return null) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/db/dao/AccountsDao.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.db.dao 2 | 3 | import androidx.room.* 4 | import com.xinto.opencord.db.entity.EntityAccount 5 | 6 | @Dao 7 | interface AccountsDao { 8 | // --------------- Inserts --------------- 9 | @Insert( 10 | onConflict = OnConflictStrategy.REPLACE, 11 | entity = EntityAccount::class, 12 | ) 13 | fun insertAccount(account: EntityAccount) 14 | 15 | // --------------- Updates --------------- 16 | @Query("UPDATE accounts SET cookies = :cookies WHERE token = :token") 17 | fun setCookies(token: String, cookies: String) 18 | 19 | @Query("UPDATE accounts SET locale = :locale WHERE token = :token") 20 | fun setLocale(token: String, locale: String?) 21 | 22 | // --------------- Queries --------------- 23 | @Query("SELECT * FROM accounts WHERE token = :token LIMIT 1") 24 | fun getAccount(token: String): EntityAccount? 25 | 26 | @Query("SELECT cookies FROM accounts WHERE token = :token LIMIT 1") 27 | fun getCookies(token: String): String? 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/db/dao/AttachmentsDao.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.db.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import com.xinto.opencord.db.entity.message.EntityAttachment 8 | 9 | @Dao 10 | interface AttachmentsDao { 11 | // --------------- Inserts --------------- 12 | @Insert( 13 | onConflict = OnConflictStrategy.REPLACE, 14 | entity = EntityAttachment::class, 15 | ) 16 | fun insertAttachments(attachments: List) 17 | 18 | // --------------- Deletes --------------- 19 | @Query("DELETE FROM attachments WHERE message_id = :messageId") 20 | fun deleteAttachments(messageId: Long) 21 | 22 | @Query("DELETE FROM attachments") 23 | fun clear() 24 | 25 | // --------------- Queries --------------- 26 | @Query("SELECT * FROM attachments WHERE id = :messageId") 27 | fun getAttachments(messageId: Long): List 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/db/dao/EmbedsDao.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.db.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import com.xinto.opencord.db.entity.message.EntityEmbed 8 | 9 | @Dao 10 | interface EmbedsDao { 11 | // --------------- Inserts --------------- 12 | @Insert( 13 | onConflict = OnConflictStrategy.REPLACE, 14 | entity = EntityEmbed::class, 15 | ) 16 | fun insertEmbeds(embeds: List) 17 | 18 | // --------------- Deletes --------------- 19 | @Query("DELETE FROM embeds WHERE message_id = :messageId") 20 | fun deleteEmbeds(messageId: Long) 21 | 22 | @Query("DELETE FROM embeds WHERE message_id = :messageId AND embed_index >= :embedCount") 23 | fun deleteTrailingEmbeds(messageId: Long, embedCount: Int) 24 | 25 | @Query("DELETE FROM embeds") 26 | fun clear() 27 | 28 | // --------------- Queries --------------- 29 | @Query("SELECT * FROM embeds WHERE message_id = :messageId ORDER BY embed_index ASC") 30 | fun getEmbeds(messageId: Long): List 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/db/dao/GuildsDao.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.db.dao 2 | 3 | import androidx.room.* 4 | import com.xinto.opencord.db.entity.guild.EntityGuild 5 | 6 | @Dao 7 | interface GuildsDao { 8 | // --------------- Inserts --------------- 9 | @Insert( 10 | onConflict = OnConflictStrategy.REPLACE, 11 | entity = EntityGuild::class, 12 | ) 13 | fun insertGuild(guild: EntityGuild) 14 | 15 | @Insert( 16 | onConflict = OnConflictStrategy.REPLACE, 17 | entity = EntityGuild::class, 18 | ) 19 | fun insertGuilds(guilds: List) 20 | 21 | @Transaction 22 | fun replaceAllGuilds(guilds: List) { 23 | clear() 24 | insertGuilds(guilds) 25 | } 26 | 27 | // --------------- Deletes --------------- 28 | @Query("DELETE FROM guilds WHERE id = :guildId") 29 | fun deleteGuild(guildId: Long) 30 | 31 | @Query("DELETE FROM guilds") 32 | fun clear() 33 | 34 | // --------------- Queries --------------- 35 | @Query("SELECT * FROM guilds WHERE id = :guildId LIMIT 1") 36 | fun getGuild(guildId: Long): EntityGuild? 37 | 38 | @Query("SELECT * FROM guilds") 39 | fun getGuilds(): List 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/db/dao/UnreadStatesDao.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.db.dao 2 | 3 | import androidx.room.* 4 | import com.xinto.opencord.db.entity.channel.EntityUnreadState 5 | 6 | @Dao 7 | interface UnreadStatesDao { 8 | // --------------- Inserts --------------- 9 | @Insert( 10 | onConflict = OnConflictStrategy.REPLACE, 11 | entity = EntityUnreadState::class, 12 | ) 13 | fun insertState(state: EntityUnreadState) 14 | 15 | @Insert( 16 | onConflict = OnConflictStrategy.REPLACE, 17 | entity = EntityUnreadState::class, 18 | ) 19 | fun insertStates(states: List) 20 | 21 | @Transaction 22 | fun replaceAllStates(states: List) { 23 | clear() 24 | insertStates(states) 25 | } 26 | 27 | // --------------- Updates --------------- 28 | @Query("UPDATE unread_states SET mention_count = coalesce(mention_count, 0) + 1 WHERE channel_id = :channelId") 29 | fun incrementMentionCount(channelId: Long) 30 | 31 | // --------------- Deletes --------------- 32 | @Query("DELETE FROM unread_states WHERE channel_id = :channelId") 33 | fun deleteUnreadState(channelId: Long) 34 | 35 | @Query("DELETE FROM unread_states") 36 | fun clear() 37 | 38 | // --------------- Queries --------------- 39 | @Query("SELECT * from unread_states WHERE channel_id = :channelId LIMIT 1") 40 | fun getUnreadState(channelId: Long): EntityUnreadState? 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/db/dao/UsersDao.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.db.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import com.xinto.opencord.db.entity.user.EntityUser 8 | 9 | @Dao 10 | interface UsersDao { 11 | // --------------- Inserts --------------- 12 | @Insert( 13 | onConflict = OnConflictStrategy.REPLACE, 14 | entity = EntityUser::class, 15 | ) 16 | fun insertUsers(users: List) 17 | 18 | // --------------- Deletes --------------- 19 | @Query("DELETE FROM USERS WHERE id NOT IN(SELECT author_id FROM MESSAGES)") 20 | fun deleteUnusedUsers() 21 | 22 | // --------------- Queries --------------- 23 | @Query("SELECT * FROM users WHERE id = :userId LIMIT 1") 24 | fun getUser(userId: Long): EntityUser? 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/db/database/AccountDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.db.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import com.xinto.opencord.db.dao.AccountsDao 6 | import com.xinto.opencord.db.entity.EntityAccount 7 | 8 | @Database( 9 | version = 1, 10 | entities = [ 11 | EntityAccount::class, 12 | ], 13 | exportSchema = false, 14 | ) 15 | abstract class AccountDatabase : RoomDatabase() { 16 | abstract fun accounts(): AccountsDao 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/db/database/CacheDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.db.database 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import androidx.room.TypeConverters 6 | import com.xinto.opencord.db.Converters 7 | import com.xinto.opencord.db.dao.* 8 | import com.xinto.opencord.db.entity.channel.EntityChannel 9 | import com.xinto.opencord.db.entity.channel.EntityUnreadState 10 | import com.xinto.opencord.db.entity.guild.EntityGuild 11 | import com.xinto.opencord.db.entity.message.EntityAttachment 12 | import com.xinto.opencord.db.entity.message.EntityEmbed 13 | import com.xinto.opencord.db.entity.message.EntityMessage 14 | import com.xinto.opencord.db.entity.reactions.EntityReaction 15 | import com.xinto.opencord.db.entity.user.EntityUser 16 | 17 | @Database( 18 | version = 1, 19 | entities = [ 20 | EntityChannel::class, 21 | EntityGuild::class, 22 | EntityAttachment::class, 23 | EntityEmbed::class, 24 | EntityMessage::class, 25 | EntityUser::class, 26 | EntityUnreadState::class, 27 | EntityReaction::class, 28 | ], 29 | exportSchema = false, 30 | ) 31 | @TypeConverters(Converters::class) 32 | abstract class CacheDatabase : RoomDatabase() { 33 | abstract fun users(): UsersDao 34 | abstract fun guilds(): GuildsDao 35 | abstract fun channels(): ChannelsDao 36 | abstract fun unreadStates(): UnreadStatesDao 37 | 38 | abstract fun messages(): MessagesDao 39 | abstract fun reactions(): ReactionsDao 40 | abstract fun attachments(): AttachmentsDao 41 | abstract fun embeds(): EmbedsDao 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/db/entity/EntityAccount.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.db.entity 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity( 8 | tableName = "accounts", 9 | ) 10 | data class EntityAccount( 11 | @PrimaryKey 12 | @ColumnInfo(name = "token") 13 | val token: String, 14 | 15 | @ColumnInfo(name = "user_id") 16 | val userId: Long? = null, 17 | 18 | @ColumnInfo(name = "username") 19 | val username: String? = null, 20 | 21 | @ColumnInfo(name = "discriminator") 22 | val discriminator: String? = null, 23 | 24 | @ColumnInfo(name = "avatar_url") 25 | val avatarUrl: String? = null, 26 | 27 | @ColumnInfo(name = "cookies") 28 | val cookies: String? = null, 29 | 30 | @ColumnInfo(name = "fingerprint") 31 | val fingerprint: String? = null, 32 | 33 | @ColumnInfo(name = "locale") 34 | val locale: String? = null, 35 | ) 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/db/entity/channel/EntityChannel.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.db.entity.channel 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.Index 6 | import androidx.room.PrimaryKey 7 | import com.xinto.opencord.rest.models.ApiChannel 8 | 9 | @Entity( 10 | tableName = "channels", 11 | indices = [ 12 | Index(value = ["guild_id"]), 13 | ], 14 | ) 15 | data class EntityChannel( 16 | // -------- Discord data -------- // 17 | @PrimaryKey 18 | val id: Long, 19 | 20 | @ColumnInfo(name = "guild_id") 21 | val guildId: Long, 22 | 23 | @ColumnInfo(name = "name") 24 | val name: String, 25 | 26 | @ColumnInfo(name = "type") 27 | val type: Int, 28 | 29 | @ColumnInfo(name = "position") 30 | val position: Int, 31 | 32 | @ColumnInfo(name = "parent_id") 33 | val parentId: Long?, 34 | 35 | @ColumnInfo(name = "nsfw") 36 | val nsfw: Boolean, 37 | 38 | @ColumnInfo(name = "last_message_id") 39 | val lastMessageId: Long? = null, 40 | 41 | // -------- DB relational data -------- // 42 | @ColumnInfo(name = "is_pins_stored") 43 | val pinsStored: Boolean, 44 | ) 45 | 46 | fun ApiChannel.toEntity(guildId: Long): EntityChannel { 47 | return EntityChannel( 48 | id = id.value, 49 | guildId = this.guildId?.value ?: guildId, 50 | name = name, 51 | type = type, 52 | position = position, 53 | parentId = parentId?.value, 54 | nsfw = nsfw, 55 | lastMessageId = lastMessageId?.value, 56 | pinsStored = false, 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/db/entity/channel/EntityUnreadState.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.db.entity.channel 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity( 8 | tableName = "unread_states", 9 | ) 10 | data class EntityUnreadState( 11 | @PrimaryKey 12 | @ColumnInfo(name = "channel_id") 13 | val channelId: Long, 14 | 15 | @ColumnInfo(name = "mention_count") 16 | val mentionCount: Int, 17 | 18 | @ColumnInfo(name = "last_message_id") 19 | val lastMessageId: Long, 20 | ) 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/db/entity/guild/EntityGuild.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.db.entity.guild 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import com.xinto.opencord.rest.models.ApiGuild 7 | 8 | @Entity( 9 | tableName = "guilds", 10 | ) 11 | data class EntityGuild( 12 | @PrimaryKey 13 | val id: Long, 14 | 15 | @ColumnInfo(name = "name") 16 | val name: String, 17 | 18 | @ColumnInfo(name = "icon") 19 | val icon: String?, 20 | 21 | @ColumnInfo(name = "banner") 22 | val banner: String? = null, 23 | 24 | @ColumnInfo(name = "premium_tier") 25 | val premiumTier: Int, 26 | 27 | @ColumnInfo(name = "premium_subscription_count") 28 | val premiumSubscriptionCount: Int? 29 | ) 30 | 31 | fun ApiGuild.toEntity(): EntityGuild { 32 | return EntityGuild( 33 | id = id.value, 34 | name = name, 35 | icon = icon, 36 | banner = banner, 37 | premiumTier = premiumTier, 38 | premiumSubscriptionCount = premiumSubscriptionCount, 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/db/entity/message/EntityAttachment.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.db.entity.message 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.Index 6 | import androidx.room.PrimaryKey 7 | import com.xinto.opencord.rest.models.ApiAttachment 8 | 9 | @Entity( 10 | tableName = "attachments", 11 | indices = [ 12 | Index(value = ["message_id"]), 13 | ], 14 | ) 15 | data class EntityAttachment( 16 | @PrimaryKey 17 | val id: Long, 18 | 19 | @ColumnInfo(name = "message_id") 20 | val messageId: Long, 21 | 22 | @ColumnInfo(name = "file_name") 23 | val fileName: String, 24 | 25 | @ColumnInfo(name = "size") 26 | val size: Int, 27 | 28 | @ColumnInfo(name = "url") 29 | val url: String, 30 | 31 | @ColumnInfo(name = "proxy_url") 32 | val proxyUrl: String, 33 | 34 | @ColumnInfo(name = "width") 35 | val width: Int?, 36 | 37 | @ColumnInfo(name = "height") 38 | val height: Int?, 39 | 40 | @ColumnInfo(name = "content_type") 41 | val contentType: String?, 42 | ) 43 | 44 | fun ApiAttachment.toEntity(messageId: Long): EntityAttachment { 45 | return EntityAttachment( 46 | id = id.value, 47 | messageId = messageId, 48 | fileName = filename, 49 | size = size, 50 | url = url, 51 | proxyUrl = proxyUrl, 52 | width = width, 53 | height = height, 54 | contentType = contentType, 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/db/entity/user/EntityUser.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.db.entity.user 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import com.xinto.opencord.rest.models.user.ApiUser 7 | 8 | @Entity( 9 | tableName = "users", 10 | ) 11 | data class EntityUser( 12 | @PrimaryKey 13 | val id: Long, 14 | 15 | @ColumnInfo(name = "username") 16 | val username: String, 17 | 18 | @ColumnInfo(name = "discriminator") 19 | val discriminator: String, 20 | 21 | @ColumnInfo(name = "avatar_hash") 22 | val avatarHash: String?, 23 | 24 | @ColumnInfo(name = "bot") 25 | val bot: Boolean, 26 | 27 | @ColumnInfo(name = "pronouns") 28 | val pronouns: String?, 29 | 30 | @ColumnInfo(name = "bio") 31 | val bio: String?, 32 | 33 | @ColumnInfo(name = "banner_url") 34 | val bannerUrl: String?, 35 | 36 | @ColumnInfo(name = "public_flags") 37 | val publicFlags: Int, 38 | ) 39 | 40 | fun ApiUser.toEntity(): EntityUser { 41 | return EntityUser( 42 | id = id.value, 43 | username = username, 44 | discriminator = discriminator, 45 | avatarHash = avatar, 46 | bot = bot, 47 | pronouns = pronouns, 48 | bio = bio, 49 | bannerUrl = banner, 50 | publicFlags = publicFlags ?: 0, 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/di/GatewayModule.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.di 2 | 3 | import com.xinto.opencord.gateway.DiscordGateway 4 | import com.xinto.opencord.gateway.DiscordGatewayImpl 5 | import org.koin.core.qualifier.named 6 | import org.koin.dsl.module 7 | 8 | val gatewayModule = module { 9 | single { 10 | DiscordGatewayImpl( 11 | client = get(named("gatewayHttp")), 12 | json = get(), 13 | accountManager = get(), 14 | propertyProvider = get(), 15 | logger = get(), 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/di/HCaptcha.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.di 2 | 3 | import android.content.Context 4 | import com.hcaptcha.sdk.HCaptcha 5 | import com.hcaptcha.sdk.HCaptchaConfig 6 | import com.xinto.opencord.BuildConfig 7 | import org.koin.dsl.module 8 | 9 | val hcaptchaModule = module { 10 | fun provideHCaptcha(activity: Context): HCaptcha { 11 | val config = HCaptchaConfig.builder() 12 | .siteKey(BuildConfig.CAPTCHA_KEY) // doubt this will ever change 13 | .resetOnTimeout(true) 14 | .tokenExpiration(1000000L) 15 | .build() 16 | return HCaptcha.getClient(activity).setup(config) 17 | } 18 | 19 | single { params -> provideHCaptcha(params.get()) } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/di/LoggerModule.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.di 2 | 3 | import com.xinto.opencord.util.Logger 4 | import com.xinto.opencord.util.LoggerImpl 5 | import org.koin.core.module.dsl.singleOf 6 | import org.koin.dsl.bind 7 | import org.koin.dsl.module 8 | 9 | val loggerModule = module { 10 | singleOf(::LoggerImpl) bind Logger::class 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/di/ManagerModule.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.di 2 | 3 | import android.content.Context 4 | import com.xinto.opencord.db.database.AccountDatabase 5 | import com.xinto.opencord.manager.* 6 | import org.koin.android.ext.koin.androidContext 7 | import org.koin.core.module.dsl.singleOf 8 | import org.koin.dsl.bind 9 | import org.koin.dsl.module 10 | 11 | val managerModule = module { 12 | fun provideAccountManager( 13 | context: Context, 14 | accountDatabase: AccountDatabase, 15 | ): AccountManager { 16 | return AccountManagerImpl( 17 | authPrefs = context.getSharedPreferences("auth", Context.MODE_PRIVATE), 18 | accounts = accountDatabase, 19 | ) 20 | } 21 | 22 | fun providePersistentDataManager( 23 | context: Context 24 | ): PersistentDataManager { 25 | return PersistentDataManagerImpl( 26 | persistentPrefs = context.getSharedPreferences("persistent_data", Context.MODE_PRIVATE), 27 | ) 28 | } 29 | 30 | single { provideAccountManager(androidContext(), get()) } 31 | single { providePersistentDataManager(androidContext()) } 32 | single { ActivityManagerImpl(androidContext()) } 33 | singleOf(::AccountCookieManagerImpl) bind AccountCookieManager::class 34 | singleOf(::ClipboardManagerImpl) bind ClipboardManager::class 35 | singleOf(::ToastManagerImpl) bind ToastManager::class 36 | singleOf(::DownloadManagerImpl) bind DownloadManager::class 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/di/ProviderModule.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.di 2 | 3 | import com.xinto.opencord.provider.AnonymousTelemetryProvider 4 | import com.xinto.opencord.provider.PropertyProvider 5 | import com.xinto.opencord.provider.PropertyProviderImpl 6 | import com.xinto.opencord.provider.TelemetryProvider 7 | import org.koin.core.module.dsl.singleOf 8 | import org.koin.dsl.bind 9 | import org.koin.dsl.module 10 | 11 | val providerModule = module { 12 | singleOf(::AnonymousTelemetryProvider) bind TelemetryProvider::class 13 | singleOf(::PropertyProviderImpl) bind PropertyProvider::class 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/di/ServiceModule.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.di 2 | 3 | import com.xinto.opencord.rest.service.DiscordApiService 4 | import com.xinto.opencord.rest.service.DiscordApiServiceImpl 5 | import com.xinto.opencord.rest.service.DiscordAuthService 6 | import com.xinto.opencord.rest.service.DiscordAuthServiceImpl 7 | import org.koin.core.qualifier.named 8 | import org.koin.dsl.module 9 | 10 | val serviceModule = module { 11 | single { 12 | DiscordAuthServiceImpl( 13 | client = get(named("authHttp")), 14 | ) 15 | } 16 | single { 17 | DiscordApiServiceImpl( 18 | client = get(named("apiHttp")), 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/di/SimpleASTModule.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.di 2 | 3 | import com.xinto.opencord.ast.rule.* 4 | import com.xinto.simpleast.Node 5 | import com.xinto.simpleast.Parser 6 | import com.xinto.simpleast.createParser 7 | import org.koin.dsl.module 8 | 9 | val simpleAstModule = module { 10 | fun provideSimpleAst(): Parser, Any?> { 11 | return createParser { 12 | rule(createEscapeRule()) 13 | rule(createSpoilerRule()) 14 | rule(createBoldTextRule()) 15 | rule(createUnderlineTextRule()) 16 | rule(createItalicAsteriskTextRule()) 17 | rule(createItalicUnderscoreTextRule()) 18 | rule(createStrikeThroughRule()) 19 | rule(createEmoteRule()) 20 | rule(createUserMentionRule()) 21 | rule(createEveryoneMentionRule()) 22 | rule(createChannelMentionRule()) 23 | rule(createOtherRule()) 24 | } 25 | } 26 | 27 | single { provideSimpleAst() } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/di/StoreModule.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.di 2 | 3 | import com.xinto.opencord.store.* 4 | import org.koin.core.module.dsl.singleOf 5 | import org.koin.dsl.bind 6 | import org.koin.dsl.module 7 | 8 | val storeModule = module { 9 | singleOf(::MessageStoreImpl) bind MessageStore::class 10 | singleOf(::LastMessageStoreImpl) bind LastMessageStore::class 11 | singleOf(::ChannelStoreImpl) bind ChannelStore::class 12 | singleOf(::GuildStoreImpl) bind GuildStore::class 13 | singleOf(::UserSettingsStoreImpl) bind UserSettingsStore::class 14 | singleOf(::CurrentUserStoreImpl) bind CurrentUserStore::class 15 | singleOf(::SessionStoreImpl) bind SessionStore::class 16 | singleOf(::UnreadStoreImpl) bind UnreadStore::class 17 | singleOf(::ReactionStoreImpl) bind ReactionStore::class 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/di/ViewModelModule.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.di 2 | 3 | import com.xinto.opencord.ui.viewmodel.* 4 | import org.koin.androidx.viewmodel.dsl.viewModelOf 5 | import org.koin.dsl.module 6 | 7 | val viewModelModule = module { 8 | viewModelOf(::LoginViewModel) 9 | viewModelOf(::ChatViewModel) 10 | viewModelOf(::GuildsViewModel) 11 | viewModelOf(::ChannelsViewModel) 12 | viewModelOf(::ChannelPinsViewModel) 13 | viewModelOf(::CurrentUserViewModel) 14 | viewModelOf(::MessageMenuViewModel) 15 | viewModelOf(::MentionsViewModel) 16 | viewModelOf(::ChatInputViewModel) 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/Mentionable.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain 2 | 3 | interface Mentionable { 4 | val formattedMention: String 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/activity/ActivityType.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.activity 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.github.materiiapps.enumutil.FromValue 5 | 6 | @Immutable 7 | @FromValue 8 | enum class ActivityType(val value: Int) { 9 | Game(0), 10 | Streaming(1), 11 | Listening(2), 12 | Watching(3), 13 | Custom(4), 14 | Competing(5), 15 | Unknown(-1); 16 | 17 | companion object 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/activity/DomainActivityAssets.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.activity 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.opencord.rest.models.activity.ApiActivityAssets 5 | 6 | @Immutable 7 | data class DomainActivityAssets( 8 | val largeImage: String?, 9 | val largeText: String?, 10 | val smallImage: String?, 11 | val smallText: String?, 12 | ) 13 | 14 | fun ApiActivityAssets.toDomain(): DomainActivityAssets { 15 | return DomainActivityAssets( 16 | largeImage = largeImage, 17 | largeText = largeText, 18 | smallImage = smallImage, 19 | smallText = smallText, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/activity/DomainActivityButton.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.activity 2 | 3 | //@Immutable 4 | //data class DomainActivityButton( 5 | // val label: String, 6 | // val url: String, 7 | //) 8 | 9 | //fun ApiActivityButton.toDomain(): DomainActivityButton { 10 | // return DomainActivityButton( 11 | // label = label, 12 | // url = url, 13 | // ) 14 | //} 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/activity/DomainActivityMetadata.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.activity 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.opencord.rest.models.activity.ApiActivityMetadata 5 | 6 | @Immutable 7 | data class DomainActivityMetadata( 8 | val albumId: String?, 9 | val artistIds: List?, 10 | val contextUri: String?, 11 | ) 12 | 13 | fun ApiActivityMetadata.toDomain(): DomainActivityMetadata { 14 | return DomainActivityMetadata( 15 | albumId = albumId, 16 | artistIds = artistIds, 17 | contextUri = contextUri, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/activity/DomainActivityParty.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.activity 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.opencord.rest.models.activity.ApiActivityParty 5 | 6 | @Immutable 7 | data class DomainActivityParty( 8 | val id: String?, 9 | val currentSize: Int?, 10 | val maxSize: Int?, 11 | ) 12 | 13 | fun ApiActivityParty.toDomain(): DomainActivityParty { 14 | return DomainActivityParty( 15 | id = id, 16 | currentSize = size?.get(0), 17 | maxSize = size?.get(1), 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/activity/DomainActivitySecrets.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.activity 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.opencord.rest.models.activity.ApiActivitySecrets 5 | 6 | @Immutable 7 | data class DomainActivitySecrets( 8 | val join: String?, 9 | val spectate: String?, 10 | val match: String?, 11 | ) 12 | 13 | fun ApiActivitySecrets.toDomain(): DomainActivitySecrets { 14 | return DomainActivitySecrets( 15 | join = join, 16 | spectate = spectate, 17 | match = match, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/activity/DomainActivityTimestamp.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.activity 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.opencord.rest.models.activity.ApiActivityTimestamp 5 | import kotlinx.datetime.Instant 6 | 7 | @Immutable 8 | data class DomainActivityTimestamp( 9 | val start: Instant?, 10 | val end: Instant?, 11 | ) 12 | 13 | fun ApiActivityTimestamp.toDomain(): DomainActivityTimestamp { 14 | return DomainActivityTimestamp( 15 | start = start?.let { Instant.fromEpochMilliseconds(it.toLong()) }, 16 | end = end?.let { Instant.fromEpochMilliseconds(it.toLong()) }, 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/activity/types/DomainActivityCustom.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.activity.types 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.opencord.domain.activity.ActivityType 5 | import com.xinto.opencord.domain.activity.DomainActivity 6 | import com.xinto.opencord.domain.emoji.DomainEmoji 7 | 8 | @Immutable 9 | data class DomainActivityCustom( 10 | override val name: String, 11 | override val createdAt: Long, 12 | val status: String?, 13 | val emoji: DomainEmoji?, 14 | ) : DomainActivity { 15 | override val type = ActivityType.Custom 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/activity/types/DomainActivityGame.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.activity.types 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.opencord.domain.activity.* 5 | 6 | @Immutable 7 | data class DomainActivityGame( 8 | override val name: String, 9 | override val createdAt: Long, 10 | val id: String?, 11 | val state: String, 12 | val details: String, 13 | val applicationId: Long, 14 | val party: DomainActivityParty?, 15 | val assets: DomainActivityAssets?, 16 | val secrets: DomainActivitySecrets?, 17 | val timestamps: DomainActivityTimestamp?, 18 | ) : DomainActivity { 19 | override val type = ActivityType.Game 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/activity/types/DomainActivityListening.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.activity.types 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.opencord.domain.activity.* 5 | 6 | @Immutable 7 | data class DomainActivityListening( 8 | override val name: String, 9 | override val createdAt: Long, 10 | val id: String, 11 | val flags: Int, 12 | val state: String, 13 | val details: String, 14 | val syncId: String, 15 | val party: DomainActivityParty, 16 | val assets: DomainActivityAssets, 17 | val metadata: DomainActivityMetadata?, 18 | val timestamps: DomainActivityTimestamp, 19 | ) : DomainActivity { 20 | override val type = ActivityType.Listening 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/activity/types/DomainActivityStreaming.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.activity.types 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.opencord.domain.activity.ActivityType 5 | import com.xinto.opencord.domain.activity.DomainActivity 6 | import com.xinto.opencord.domain.activity.DomainActivityAssets 7 | 8 | @Immutable 9 | data class DomainActivityStreaming( 10 | override val name: String, 11 | override val createdAt: Long, 12 | val id: String, 13 | val url: String, 14 | val state: String, 15 | val details: String, 16 | val assets: DomainActivityAssets, 17 | ) : DomainActivity { 18 | override val type = ActivityType.Streaming 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/activity/types/DomainActivityUnknown.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.activity.types 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.opencord.domain.activity.ActivityType 5 | import com.xinto.opencord.domain.activity.DomainActivity 6 | 7 | // TODO: remove this once all activity types are implemented 8 | @Immutable 9 | data class DomainActivityUnknown( 10 | override val name: String, 11 | override val createdAt: Long, 12 | ) : DomainActivity { 13 | override val type = ActivityType.Unknown 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/attachment/DomainFileAttachment.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.attachment 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | data class DomainFileAttachment( 7 | override val id: Long, 8 | override val filename: String, 9 | override val size: Int, 10 | override val url: String, 11 | override val proxyUrl: String, 12 | override val type: String?, 13 | ) : DomainAttachment 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/attachment/DomainPictureAttachment.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.attachment 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | data class DomainPictureAttachment( 7 | override val id: Long, 8 | override val filename: String, 9 | override val size: Int, 10 | override val url: String, 11 | override val proxyUrl: String, 12 | override val type: String, 13 | val width: Int, 14 | val height: Int, 15 | ) : DomainAttachment 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/attachment/DomainVideoAttachment.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.attachment 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | data class DomainVideoAttachment( 7 | override val id: Long, 8 | override val filename: String, 9 | override val size: Int, 10 | override val url: String, 11 | override val proxyUrl: String, 12 | override val type: String, 13 | val width: Int, 14 | val height: Int, 15 | ) : DomainAttachment 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/channel/DomainAnnouncementChannel.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.channel 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | data class DomainAnnouncementChannel( 7 | override val id: Long, 8 | override val guildId: Long?, 9 | override val name: String, 10 | override val position: Int, 11 | override val parentId: Long?, 12 | val nsfw: Boolean, 13 | ) : DomainChannel() { 14 | override val sortingPriority: Short get() = 2 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/channel/DomainCategoryChannel.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.channel 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | data class DomainCategoryChannel( 7 | override val id: Long, 8 | override val guildId: Long?, 9 | override val name: String, 10 | override val position: Int, 11 | override val parentId: Long? = null, 12 | ) : DomainChannel() { 13 | override val sortingPriority: Short get() = 0 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/channel/DomainTextChannel.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.channel 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | data class DomainTextChannel( 7 | override val id: Long, 8 | override val guildId: Long?, 9 | override val name: String, 10 | override val position: Int, 11 | override val parentId: Long?, 12 | val nsfw: Boolean, 13 | ) : DomainChannel() { 14 | override val sortingPriority: Short get() = 2 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/channel/DomainUnreadState.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.channel 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.opencord.db.entity.channel.EntityUnreadState 5 | 6 | @Immutable 7 | data class DomainUnreadState( 8 | val channelId: Long, 9 | val mentionCount: Int, 10 | val lastMessageId: Long, 11 | ) 12 | 13 | fun EntityUnreadState.toDomain(): DomainUnreadState { 14 | return DomainUnreadState( 15 | channelId = channelId, 16 | mentionCount = mentionCount, 17 | lastMessageId = lastMessageId, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/channel/DomainVoiceChannel.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.channel 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | data class DomainVoiceChannel( 7 | override val id: Long, 8 | override val guildId: Long?, 9 | override val name: String, 10 | override val position: Int, 11 | override val parentId: Long?, 12 | ) : DomainChannel() { 13 | override val sortingPriority: Short get() = 3 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/embed/DomainEmbedFooter.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.embed 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.opencord.rest.models.embed.ApiEmbedFooter 5 | import com.xinto.opencord.util.Timestamp 6 | import kotlinx.datetime.Instant 7 | 8 | @Immutable 9 | data class DomainEmbedFooter( 10 | val text: String, 11 | val timestamp: Instant?, 12 | val iconUrl: String?, 13 | val proxyIconUrl: String?, 14 | ) { 15 | val formattedTimestamp: String? by lazy { 16 | timestamp?.let { Timestamp.getFormattedTimestamp(it) } 17 | } 18 | 19 | val displayUrl: String? by lazy { 20 | if (proxyIconUrl != null) { 21 | "$proxyIconUrl?width=64&height=64" 22 | } else { 23 | iconUrl 24 | } 25 | } 26 | } 27 | 28 | fun ApiEmbedFooter.toDomain(timestamp: Instant?): DomainEmbedFooter { 29 | return DomainEmbedFooter( 30 | text = text, 31 | timestamp = timestamp, 32 | iconUrl = iconUrl, 33 | proxyIconUrl = proxyIconUrl, 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/emoji/DomainEmoji.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.emoji 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.github.materiiapps.partial.getOrNull 5 | import com.xinto.opencord.rest.models.emoji.ApiEmoji 6 | 7 | /** 8 | * A unique identifier of a [DomainEmoji] that is obtained through [DomainEmoji.identifier] 9 | */ 10 | @Immutable 11 | @JvmInline 12 | value class DomainEmojiIdentifier(private val hashCode: Int?) { 13 | val exists: Boolean 14 | get() = hashCode != null 15 | } 16 | 17 | @Immutable 18 | sealed interface DomainEmoji { 19 | val identifier: DomainEmojiIdentifier 20 | } 21 | 22 | fun ApiEmoji.toDomain(): DomainEmoji { 23 | return when { 24 | id == null && name != null -> DomainUnicodeEmoji( 25 | emoji = name, 26 | ) 27 | id != null -> DomainGuildEmoji( 28 | name = name, 29 | id = id.value, 30 | animated = animated.getOrNull() ?: false, 31 | ) 32 | else -> DomainUnknownEmoji 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/emoji/DomainGuildEmoji.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.emoji 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.opencord.rest.service.DiscordCdnServiceImpl 5 | 6 | @Immutable 7 | data class DomainGuildEmoji( 8 | val id: Long, 9 | val name: String?, 10 | val animated: Boolean, 11 | ) : DomainEmoji { 12 | override val identifier: DomainEmojiIdentifier 13 | get() = DomainEmojiIdentifier(id.hashCode()) 14 | 15 | val url: String by lazy { 16 | DiscordCdnServiceImpl.getGuildEmojiUrl(id, animated) 17 | } 18 | 19 | // val isDeleted: Boolean 20 | // get() = name == null 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/emoji/DomainUnicodeEmoji.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.emoji 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | data class DomainUnicodeEmoji( 7 | val emoji: String, 8 | ) : DomainEmoji { 9 | override val identifier: DomainEmojiIdentifier 10 | get() = DomainEmojiIdentifier(emoji.hashCode()) 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/emoji/DomainUnknownEmoji.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.emoji 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | object DomainUnknownEmoji : DomainEmoji { 7 | override val identifier: DomainEmojiIdentifier 8 | get() = DomainEmojiIdentifier(null) 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/login/DomainLogin.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.login 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.opencord.rest.models.login.ApiLogin 5 | 6 | @Immutable 7 | sealed interface DomainLogin { 8 | @Immutable 9 | data class Login( 10 | val token: String, 11 | val mfa: Boolean, 12 | val locale: String, 13 | val theme: String, 14 | ) : DomainLogin 15 | 16 | @Immutable 17 | data class Captcha(val captchaSiteKey: String) : DomainLogin 18 | 19 | @Immutable 20 | data class TwoFactorAuth(val ticket: String) : DomainLogin 21 | 22 | @Immutable 23 | object Error : DomainLogin 24 | } 25 | 26 | fun ApiLogin.toDomain(): DomainLogin { 27 | return when { 28 | ticket != null -> { 29 | DomainLogin.TwoFactorAuth( 30 | ticket = ticket, 31 | ) 32 | } 33 | captchaSiteKey != null -> { 34 | DomainLogin.Captcha( 35 | captchaSiteKey = captchaSiteKey, 36 | ) 37 | } 38 | token != null -> { 39 | DomainLogin.Login( 40 | token = token, 41 | mfa = mfa, 42 | theme = userSettings?.theme!!, 43 | locale = userSettings.locale, 44 | ) 45 | } 46 | else -> DomainLogin.Error 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/member/DomainGuildMember.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.member 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.opencord.domain.user.DomainUser 5 | import com.xinto.opencord.domain.user.toDomain 6 | import com.xinto.opencord.rest.models.ApiGuildMember 7 | import com.xinto.opencord.rest.service.DiscordCdnServiceImpl 8 | 9 | @Immutable 10 | data class DomainGuildMember( 11 | val user: DomainUser?, 12 | val nick: String? = null, 13 | val avatarUrl: String? = null, 14 | ) 15 | 16 | fun ApiGuildMember.toDomain(): DomainGuildMember { 17 | val avatarUrl = user?.let { user -> 18 | avatar 19 | ?.let { DiscordCdnServiceImpl.getUserAvatarUrl(user.id.value, it) } 20 | ?: DiscordCdnServiceImpl.getDefaultAvatarUrl(user.discriminator.toInt().rem(5)) 21 | } 22 | return DomainGuildMember( 23 | user = user?.toDomain(), 24 | nick = nick, 25 | avatarUrl = avatarUrl, 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/message/DomainMessageMemberJoin.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.message 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.github.materiiapps.partial.Partialize 5 | import com.xinto.opencord.domain.user.DomainUser 6 | import com.xinto.opencord.util.SimpleAstParser 7 | import com.xinto.opencord.util.Timestamp 8 | import com.xinto.simpleast.render 9 | import kotlinx.datetime.Instant 10 | import org.koin.core.component.KoinComponent 11 | import org.koin.core.component.get 12 | 13 | @Immutable 14 | @Partialize // Is this even able to be partial? 15 | data class DomainMessageMemberJoin( 16 | override val id: Long, 17 | override val channelId: Long, 18 | override val guildId: Long?, 19 | override val timestamp: Instant, 20 | override val pinned: Boolean, 21 | override val content: String, 22 | override val author: DomainUser 23 | ) : DomainMessage, KoinComponent { 24 | override val contentRendered by lazy { 25 | render( 26 | nodes = get() 27 | .parse(content, null), 28 | renderContext = null, 29 | ).toAnnotatedString() 30 | } 31 | override val formattedTimestamp by lazy { 32 | Timestamp.getFormattedTimestamp(timestamp) 33 | } 34 | override val isDeletable get() = true 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/message/DomainMessageUnknown.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.message 2 | 3 | import androidx.compose.runtime.Immutable 4 | import androidx.compose.ui.text.buildAnnotatedString 5 | import com.github.materiiapps.partial.Partialize 6 | import com.xinto.opencord.domain.user.DomainUser 7 | import com.xinto.opencord.util.Timestamp 8 | import kotlinx.datetime.Instant 9 | 10 | @Immutable 11 | @Partialize 12 | data class DomainMessageUnknown( 13 | override val id: Long, 14 | override val channelId: Long, 15 | override val guildId: Long?, 16 | override val timestamp: Instant, 17 | override val pinned: Boolean, 18 | override val content: String, 19 | override val author: DomainUser 20 | ) : DomainMessage { 21 | override val contentRendered get() = buildAnnotatedString {} 22 | override val formattedTimestamp by lazy { 23 | Timestamp.getFormattedTimestamp(timestamp) 24 | } 25 | override val isDeletable get() = true 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/message/reaction/DomainReaction.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.message.reaction 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.opencord.db.entity.reactions.EntityReaction 5 | import com.xinto.opencord.domain.emoji.* 6 | import com.xinto.opencord.rest.models.reaction.ApiReaction 7 | 8 | @Immutable 9 | data class DomainReaction( 10 | val count: Int, 11 | val meReacted: Boolean, 12 | val emoji: DomainEmoji, 13 | ) 14 | 15 | fun ApiReaction.toDomain(): DomainReaction { 16 | return DomainReaction( 17 | count = count, 18 | meReacted = meReacted, 19 | emoji = emoji.toDomain(), 20 | ) 21 | } 22 | 23 | fun EntityReaction.toDomain(): DomainReaction { 24 | val emoji = when { 25 | emojiId > 0L -> DomainGuildEmoji( 26 | id = emojiId, 27 | name = emojiName, 28 | animated = animated, 29 | ) 30 | emojiName.isNotEmpty() -> DomainUnicodeEmoji( 31 | emoji = emojiName, 32 | ) 33 | else -> DomainUnknownEmoji 34 | } 35 | 36 | return DomainReaction( 37 | count = count, 38 | meReacted = meReacted, 39 | emoji = emoji, 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/user/DomainUserPrivate.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.user 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | data class DomainUserPrivate( 7 | override val id: Long, 8 | override val username: String, 9 | override val discriminator: String, 10 | override val avatarUrl: String, 11 | override val bot: Boolean, 12 | override val bio: String?, 13 | override val flags: Int, 14 | val pronouns: String?, 15 | val mfaEnabled: Boolean, 16 | val verified: Boolean, 17 | val email: String, 18 | val phone: String?, // TODO: verify this is nullable 19 | val locale: String, 20 | ) : DomainUser() 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/user/DomainUserPrivateReady.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.user 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | /** 6 | * Sent on the `user` field of the READY event 7 | */ 8 | @Immutable 9 | data class DomainUserPrivateReady( 10 | override val id: Long, 11 | override val username: String, 12 | override val discriminator: String, 13 | override val avatarUrl: String, 14 | override val bot: Boolean, 15 | override val bio: String?, 16 | override val flags: Int, 17 | val mfaEnabled: Boolean, 18 | val verified: Boolean, 19 | val premium: Boolean, 20 | val purchasedFlags: Int, // TODO: implement bitfield 21 | ) : DomainUser() 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/user/DomainUserPublic.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.user 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | @Immutable 6 | data class DomainUserPublic( 7 | override val id: Long, 8 | override val username: String, 9 | override val discriminator: String, 10 | override val avatarUrl: String, 11 | override val bot: Boolean, 12 | override val bio: String?, 13 | override val flags: Int, 14 | val pronouns: String?, 15 | ) : DomainUser() 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/usersettings/DomainCustomStatus.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.usersettings 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.opencord.rest.models.user.settings.ApiCustomStatus 5 | import kotlinx.datetime.Instant 6 | 7 | @Immutable 8 | data class DomainCustomStatus( 9 | val text: String?, 10 | val expiresAt: Instant?, 11 | val emojiId: Long?, 12 | val emojiName: String? 13 | ) 14 | 15 | fun ApiCustomStatus.toDomain(): DomainCustomStatus { 16 | return DomainCustomStatus( 17 | text = text, 18 | expiresAt = expiresAt?.let { Instant.parse(it) }, 19 | emojiId = emojiId?.value, 20 | emojiName = emojiName, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/usersettings/DomainFriendSources.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.usersettings 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.opencord.rest.models.user.settings.ApiFriendSources 5 | 6 | @Immutable 7 | data class DomainFriendSources( 8 | val all: Boolean, 9 | val mutualFriends: Boolean, 10 | val mutualGuilds: Boolean, 11 | ) 12 | 13 | fun ApiFriendSources.toDomain(): DomainFriendSources { 14 | return DomainFriendSources( 15 | all = (all ?: false) && mutualFriends == null && mutualGuilds == null, 16 | mutualFriends = mutualFriends ?: false, 17 | mutualGuilds = mutualGuilds ?: false, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/usersettings/DomainGuildFolder.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.usersettings 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.xinto.opencord.rest.models.user.settings.ApiGuildFolder 5 | 6 | @Immutable 7 | data class DomainGuildFolder( 8 | val id: Long?, 9 | val guildIds: List, 10 | val name: String?, 11 | ) 12 | 13 | fun ApiGuildFolder.toDomain(): DomainGuildFolder { 14 | return DomainGuildFolder( 15 | id = id?.value, 16 | guildIds = guildIds.map { it.value }, 17 | name = name, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/usersettings/DomainThemeSetting.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.usersettings 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.github.materiiapps.enumutil.FromValue 5 | 6 | @Immutable 7 | @FromValue 8 | enum class DomainThemeSetting(val value: String) { 9 | Dark("dark"), 10 | Light("light"); 11 | 12 | companion object 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/domain/usersettings/DomainUserStatus.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.domain.usersettings 2 | 3 | import androidx.compose.runtime.Immutable 4 | import com.github.materiiapps.enumutil.FromValue 5 | 6 | @Immutable 7 | @FromValue 8 | enum class DomainUserStatus(val value: String) { 9 | Online("online"), 10 | Idle("idle"), 11 | Dnd("dnd"), 12 | Invisible("invisible"); 13 | 14 | companion object 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/CloseCode.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway 2 | 3 | import com.github.materiiapps.enumutil.FromValue 4 | 5 | @FromValue 6 | enum class CloseCode(val code: Int) { 7 | Unknown(-1), 8 | UnknownError(4000), 9 | UnknownOpCode(4001), 10 | DecodeError(4002), 11 | NotAuthorized(4003), 12 | AuthenticationFailed(4004), 13 | AlreadyAuthenticated(4005), 14 | InvalidSequenceNumber(4007), 15 | RateLimited(4008), 16 | SessionTimedOut(4009); 17 | 18 | companion object 19 | } 20 | 21 | val CloseCode?.canReconnect: Boolean 22 | get() { 23 | return when (this) { 24 | CloseCode.Unknown -> false 25 | CloseCode.UnknownError -> true 26 | CloseCode.UnknownOpCode -> true 27 | CloseCode.DecodeError -> true 28 | CloseCode.NotAuthorized -> true 29 | CloseCode.AuthenticationFailed -> false 30 | CloseCode.AlreadyAuthenticated -> true 31 | CloseCode.InvalidSequenceNumber -> true 32 | CloseCode.RateLimited -> true 33 | CloseCode.SessionTimedOut -> true 34 | else -> false 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/dto/GuildDeleteData.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.dto 2 | 3 | import com.xinto.opencord.rest.models.ApiSnowflake 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class GuildDeleteData( 9 | @SerialName("id") 10 | val id: ApiSnowflake, 11 | 12 | @SerialName("unavailable") 13 | val unavailable: Boolean, 14 | ) 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/dto/Heartbeat.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.dto 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Heartbeat( 8 | @SerialName("heartbeat_interval") 9 | val heartbeatInterval: Long, 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/dto/MessageAckData.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.dto 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class MessageAckData( 8 | @SerialName("ack_type") 9 | val ackType: Int? = null, 10 | 11 | @SerialName("channel_id") 12 | val channelId: Long, 13 | 14 | @SerialName("message_id") 15 | val messageId: Long, 16 | 17 | @SerialName("version") 18 | val version: Int, 19 | 20 | @SerialName("mention_count") 21 | val mentionCount: Int? = 0, 22 | ) 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/dto/MessageDeleteData.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.dto 2 | 3 | import com.xinto.opencord.rest.models.ApiSnowflake 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class MessageDeleteData( 9 | @SerialName("id") 10 | val messageId: ApiSnowflake, 11 | 12 | @SerialName("channel_id") 13 | val channelId: ApiSnowflake, 14 | 15 | @SerialName("guild_id") 16 | val guildId: ApiSnowflake, 17 | ) 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/dto/Presence.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.dto 2 | 3 | import com.xinto.opencord.rest.models.activity.ApiActivity 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class UpdatePresence( 8 | val since: Long, 9 | val status: String, 10 | val afk: Boolean?, 11 | val activities: List, 12 | ) 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/dto/Reaction.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.dto 2 | 3 | import com.xinto.opencord.rest.models.ApiSnowflake 4 | import com.xinto.opencord.rest.models.emoji.ApiEmoji 5 | import kotlinx.serialization.SerialName 6 | import kotlinx.serialization.Serializable 7 | 8 | @Serializable 9 | data class MessageReactionAddData( 10 | @SerialName("user_id") 11 | val userId: ApiSnowflake, 12 | 13 | @SerialName("channel_id") 14 | val channelId: ApiSnowflake, 15 | 16 | @SerialName("message_id") 17 | val messageId: ApiSnowflake, 18 | 19 | @SerialName("guild_id") 20 | val guildId: ApiSnowflake? = null, 21 | 22 | @SerialName("emoji") 23 | val emoji: ApiEmoji, 24 | 25 | // @SerialName("member") 26 | // val member: ApiGuildMember? = null, 27 | ) 28 | 29 | @Serializable 30 | data class MessageReactionRemoveData( 31 | @SerialName("user_id") 32 | val userId: ApiSnowflake, 33 | 34 | @SerialName("channel_id") 35 | val channelId: ApiSnowflake, 36 | 37 | @SerialName("message_id") 38 | val messageId: ApiSnowflake, 39 | 40 | @SerialName("guild_id") 41 | val guildId: ApiSnowflake? = null, 42 | 43 | @SerialName("emoji") 44 | val emoji: ApiEmoji, 45 | ) 46 | 47 | @Serializable 48 | data class MessageReactionRemoveAllData( 49 | @SerialName("message_id") 50 | val messageId: ApiSnowflake, 51 | 52 | @SerialName("channel_id") 53 | val channelId: ApiSnowflake, 54 | 55 | @SerialName("guild_id") 56 | val guildId: ApiSnowflake? = null, 57 | ) 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/dto/Ready.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.dto 2 | 3 | import com.xinto.opencord.rest.models.ApiGuild 4 | import com.xinto.opencord.rest.models.user.ApiUser 5 | import com.xinto.opencord.rest.models.user.settings.ApiUserSettings 6 | import kotlinx.datetime.Instant 7 | import kotlinx.serialization.SerialName 8 | import kotlinx.serialization.Serializable 9 | 10 | @Serializable 11 | data class Ready( 12 | @SerialName("user") 13 | val user: ApiUser, 14 | 15 | @SerialName("guilds") 16 | val guilds: List, 17 | 18 | @SerialName("sessions") 19 | val sessions: List, 20 | 21 | @SerialName("session_id") 22 | val sessionId: String, 23 | 24 | @SerialName("user_settings") 25 | val userSettings: ApiUserSettings, 26 | 27 | @SerialName("read_state") 28 | val readState: ReadState, 29 | 30 | @SerialName("auth_token") 31 | val newAuthToken: String? = null, 32 | ) 33 | 34 | @Serializable 35 | data class ReadState( 36 | val version: Int, 37 | val partial: Boolean, 38 | val entries: List 39 | ) 40 | 41 | @Serializable 42 | data class ReadStateEntry( 43 | @SerialName("id") 44 | val channelId: Long, 45 | 46 | @SerialName("mention_count") 47 | val mentionCount: Int, 48 | 49 | @SerialName("last_pin_timestamp") 50 | val lastPinTimestamp: Instant, 51 | 52 | @SerialName("last_message_id") 53 | val lastMessageId: Long, 54 | ) 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/dto/RequestGuildMembers.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.dto 2 | 3 | import com.xinto.opencord.rest.models.ApiSnowflake 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class RequestGuildMembers( 9 | @SerialName("guild_id") 10 | val guildId: ApiSnowflake, 11 | 12 | @SerialName("query") 13 | val query: String = "", 14 | 15 | @SerialName("limit") 16 | val limit: Int = 0, 17 | 18 | @SerialName("presences") 19 | val presences: Boolean? = null, 20 | 21 | @SerialName("user_ids") 22 | val userIds: List? = null, 23 | ) 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/dto/Resume.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.dto 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Resume( 8 | @SerialName("token") 9 | val token: String, 10 | 11 | @SerialName("session_id") 12 | val sessionId: String, 13 | 14 | @SerialName("seq") 15 | val sequenceNumber: Int 16 | ) 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/dto/SessionsReplaceData.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.dto 2 | 3 | import com.xinto.opencord.rest.models.activity.ApiActivity 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | typealias ReplaceSessionsData = List 8 | 9 | @Serializable 10 | data class SessionData( 11 | @SerialName("session_id") 12 | val sessionId: String, 13 | 14 | @SerialName("client_info") 15 | val clientInfo: ClientInfo, 16 | 17 | @SerialName("status") 18 | val status: String, 19 | 20 | @SerialName("activities") 21 | val activities: List, 22 | ) 23 | 24 | @Serializable 25 | data class ClientInfo( 26 | val client: String, 27 | val os: String, 28 | ) 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/event/Channel.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.event 2 | 3 | import com.xinto.opencord.rest.models.ApiChannel 4 | 5 | data class ChannelCreateEvent( 6 | val data: ApiChannel, 7 | ) : Event 8 | 9 | data class ChannelUpdateEvent( 10 | val data: ApiChannel, 11 | ) : Event 12 | 13 | data class ChannelDeleteEvent( 14 | val data: ApiChannel, 15 | ) : Event 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/event/Guild.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.event 2 | 3 | import com.xinto.opencord.gateway.dto.GuildDeleteData 4 | import com.xinto.opencord.rest.models.ApiGuild 5 | 6 | data class GuildCreateEvent( 7 | val data: ApiGuild 8 | ) : Event 9 | 10 | data class GuildUpdateEvent( 11 | val data: ApiGuild 12 | ) : Event 13 | 14 | data class GuildDeleteEvent( 15 | val data: GuildDeleteData 16 | ) : Event 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/event/GuildMemberChunk.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.event 2 | 3 | import com.xinto.opencord.rest.models.ApiGuildMemberChunk 4 | 5 | data class GuildMemberChunkEvent( 6 | val data: ApiGuildMemberChunk 7 | ) : Event 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/event/Message.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.event 2 | 3 | import com.xinto.opencord.gateway.dto.MessageAckData 4 | import com.xinto.opencord.gateway.dto.MessageDeleteData 5 | import com.xinto.opencord.rest.models.message.ApiMessage 6 | import com.xinto.opencord.rest.models.message.ApiMessagePartial 7 | 8 | data class MessageCreateEvent( 9 | val data: ApiMessage, 10 | ) : Event 11 | 12 | data class MessageUpdateEvent( 13 | val data: ApiMessagePartial, 14 | ) : Event 15 | 16 | data class MessageDeleteEvent( 17 | val data: MessageDeleteData, 18 | ) : Event 19 | 20 | data class MessageAckEvent( 21 | val data: MessageAckData, 22 | ) : Event 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/event/Reaction.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.event 2 | 3 | import com.xinto.opencord.gateway.dto.MessageReactionAddData 4 | import com.xinto.opencord.gateway.dto.MessageReactionRemoveAllData 5 | import com.xinto.opencord.gateway.dto.MessageReactionRemoveData 6 | 7 | data class MessageReactionAddEvent( 8 | val data: MessageReactionAddData 9 | ) : Event 10 | 11 | data class MessageReactionRemoveEvent( 12 | val data: MessageReactionRemoveData 13 | ) : Event 14 | 15 | data class MessageReactionRemoveAllEvent( 16 | val data: MessageReactionRemoveAllData, 17 | ) : Event 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/event/Ready.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.event 2 | 3 | import com.xinto.opencord.gateway.dto.Ready 4 | 5 | data class ReadyEvent( 6 | val data: Ready 7 | ) : Event 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/event/Sessions.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.event 2 | 3 | import com.xinto.opencord.gateway.dto.ReplaceSessionsData 4 | 5 | data class SessionsReplaceEvent( 6 | val data: ReplaceSessionsData, 7 | ) : Event 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/event/UserSettings.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.event 2 | 3 | import com.xinto.opencord.rest.models.user.settings.ApiUserSettingsPartial 4 | 5 | data class UserSettingsUpdateEvent( 6 | val data: ApiUserSettingsPartial 7 | ) : Event 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/event/UserUpdateEvent.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.event 2 | 3 | import com.xinto.opencord.rest.models.user.ApiUser 4 | 5 | data class UserUpdateEvent( 6 | val data: ApiUser 7 | ) : Event 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/io/Capabilities.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.io 2 | 3 | @Suppress("unused") 4 | enum class Capabilities(val value: Int) { 5 | LAZY_USER_NOTES(1 shl 0), 6 | NO_AFFINE_USER_IDS(1 shl 1), 7 | VERSIONED_READ_STATES(1 shl 2), 8 | VERSIONED_USER_GUILD_SETTINGS(1 shl 3), 9 | DEDUPLICATE_USER_OBJECTS(1 shl 4), 10 | PRIORITIZED_READY_PAYLOAD(1 shl 5), 11 | MULTIPLE_GUILD_EXPERIMENT_POPULATIONS(1 shl 6), 12 | NON_CHANNEL_READ_STATES(1 shl 7), 13 | AUTH_TOKEN_REFRESH(1 shl 8), 14 | USER_SETTINGS_PROTO(1 shl 9), 15 | CLIENT_STATE_V2(1 shl 10), 16 | PASSIVE_GUILD_UPDATE(1 shl 11), 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/io/OpCode.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.io 2 | 3 | import com.github.materiiapps.enumutil.FromValue 4 | import kotlinx.serialization.KSerializer 5 | import kotlinx.serialization.Serializable 6 | import kotlinx.serialization.descriptors.PrimitiveKind 7 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 8 | import kotlinx.serialization.descriptors.SerialDescriptor 9 | import kotlinx.serialization.encoding.Decoder 10 | import kotlinx.serialization.encoding.Encoder 11 | 12 | @FromValue 13 | @Serializable(OpCode.Serializer::class) 14 | enum class OpCode(val code: Int) { 15 | Dispatch(0), 16 | Heartbeat(1), 17 | Identify(2), 18 | PresenceUpdate(3), 19 | VoiceStateUpdate(4), 20 | Resume(6), 21 | Reconnect(7), 22 | RequestGuildMembers(8), 23 | InvalidSession(9), 24 | Hello(10), 25 | HeartbeatAck(11); 26 | 27 | companion object Serializer : KSerializer { 28 | override val descriptor: SerialDescriptor 29 | get() = PrimitiveSerialDescriptor("OpCode", PrimitiveKind.INT) 30 | 31 | override fun deserialize(decoder: Decoder): OpCode { 32 | val opCode = decoder.decodeInt() 33 | return fromValue(opCode) ?: throw IllegalArgumentException("Unknown OpCode $opCode") 34 | } 35 | 36 | override fun serialize(encoder: Encoder, value: OpCode) { 37 | encoder.encodeInt(value.code) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/gateway/io/Payload.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.gateway.io 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.json.JsonElement 6 | 7 | @Serializable 8 | data class IncomingPayload( 9 | @SerialName("op") 10 | val opCode: OpCode, 11 | 12 | @SerialName("d") 13 | val data: JsonElement?, 14 | 15 | @SerialName("s") 16 | val sequenceNumber: Int?, 17 | 18 | @SerialName("t") 19 | val eventName: EventName?, 20 | ) 21 | 22 | @Serializable 23 | data class OutgoingPayload( 24 | @SerialName("op") 25 | val opCode: OpCode, 26 | 27 | @SerialName("d") 28 | val data: T?, 29 | ) 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/manager/ActivityManager.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.manager 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import kotlin.reflect.KClass 7 | 8 | interface ActivityManager { 9 | fun startActivity(activity: KClass, clearOld: Boolean = true) 10 | } 11 | 12 | class ActivityManagerImpl( 13 | private val appContext: Context 14 | ) : ActivityManager { 15 | override fun startActivity(activity: KClass, clearOld: Boolean) { 16 | val flags = if (clearOld) { 17 | Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 18 | } else { 19 | Intent.FLAG_ACTIVITY_NEW_TASK 20 | } 21 | 22 | val intent = Intent(appContext, activity.java) 23 | .apply { this.flags = flags } 24 | 25 | appContext.startActivity(intent) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/manager/ClipboardManager.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.manager 2 | 3 | import android.content.ClipData 4 | import android.content.Context 5 | import android.net.Uri 6 | import androidx.core.content.getSystemService 7 | 8 | interface ClipboardManager { 9 | fun setText(text: String) 10 | fun setLink(url: String) 11 | } 12 | 13 | class ClipboardManagerImpl( 14 | appContext: Context, 15 | ) : ClipboardManager { 16 | private val clipboard = appContext.getSystemService() 17 | ?: error("could not get ClipboardManager") 18 | 19 | override fun setText(text: String) { 20 | clipboard.setPrimaryClip(ClipData.newPlainText(null, text)) 21 | } 22 | 23 | override fun setLink(url: String) { 24 | clipboard.setPrimaryClip(ClipData.newRawUri(null, Uri.parse(url))) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/manager/ToastManager.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.manager 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | import androidx.annotation.StringRes 6 | 7 | interface ToastManager { 8 | fun showToast( 9 | @StringRes stringId: Int, 10 | vararg args: Any, 11 | duration: Int = Toast.LENGTH_LONG 12 | ) 13 | 14 | fun showToast( 15 | text: CharSequence, 16 | duration: Int = Toast.LENGTH_LONG, 17 | ) 18 | } 19 | 20 | class ToastManagerImpl( 21 | private val appContext: Context, 22 | ) : ToastManager { 23 | override fun showToast(stringId: Int, vararg args: Any, duration: Int) { 24 | Toast.makeText( 25 | appContext, 26 | appContext.getString(stringId, *args), 27 | duration, 28 | ).show() 29 | } 30 | 31 | override fun showToast(text: CharSequence, duration: Int) { 32 | Toast.makeText( 33 | appContext, 34 | text, 35 | duration, 36 | ).show() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/manager/base/BasePreferenceManager.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.manager.base 2 | 3 | import android.content.SharedPreferences 4 | import androidx.core.content.edit 5 | 6 | abstract class BasePreferenceManager( 7 | private val prefs: SharedPreferences 8 | ) { 9 | protected fun getString(key: String, defaultValue: String?): String? { 10 | return prefs.getString(key, defaultValue) 11 | } 12 | 13 | protected fun getBoolean(key: String, defaultValue: Boolean): Boolean { 14 | return prefs.getBoolean(key, defaultValue) 15 | } 16 | 17 | protected fun getLong(key: String, defaultValue: Long): Long { 18 | return prefs.getLong(key, defaultValue) 19 | } 20 | 21 | protected fun getInt(key: String, defaultValue: Int): Int { 22 | return prefs.getInt(key, defaultValue) 23 | } 24 | 25 | protected fun getStringSet(key: String, defaultValue: Set): Set? { 26 | return prefs.getStringSet(key, defaultValue) 27 | } 28 | 29 | protected fun putString(key: String, value: String?) { 30 | return prefs.edit { putString(key, value) } 31 | } 32 | 33 | protected fun putBoolean(key: String, value: Boolean) { 34 | return prefs.edit { putBoolean(key, value) } 35 | } 36 | 37 | protected fun putLong(key: String, value: Long) { 38 | return prefs.edit { putLong(key, value) } 39 | } 40 | 41 | protected fun putInt(key: String, value: Int) { 42 | return prefs.edit { putInt(key, value) } 43 | } 44 | 45 | protected fun putStringSet(key: String, value: Set) { 46 | prefs.edit { putStringSet(key, value) } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/body/Login.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.body 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class LoginBody( 8 | @SerialName("login") 9 | val login: String, 10 | 11 | @SerialName("password") 12 | val password: String, 13 | 14 | @SerialName("undelete") 15 | val undelete: Boolean, 16 | 17 | @SerialName("captcha_key") 18 | val captchaKey: String? = null, 19 | ) 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/body/Message.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.body 2 | 3 | import com.xinto.opencord.rest.models.ApiSnowflake 4 | import com.xinto.opencord.util.NonceGenerator 5 | import kotlinx.serialization.EncodeDefault 6 | import kotlinx.serialization.ExperimentalSerializationApi 7 | import kotlinx.serialization.SerialName 8 | import kotlinx.serialization.Serializable 9 | 10 | @OptIn(ExperimentalSerializationApi::class) 11 | @Serializable 12 | data class MessageBody( 13 | val content: String, 14 | val flags: Int = 0, 15 | val tts: Boolean = false, 16 | 17 | @EncodeDefault 18 | @SerialName("sticker_ids") 19 | val stickers: List = emptyList(), 20 | 21 | @EncodeDefault 22 | val nonce: String = NonceGenerator.nowSnowflake(), 23 | ) 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/body/TwoFactor.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.body 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class TwoFactorBody( 8 | @SerialName("code") 9 | val code: String, 10 | 11 | @SerialName("ticket") 12 | val ticket: String, 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/body/XSuperProperties.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.body 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class XSuperProperties( 8 | @SerialName("browser") 9 | val browser: String, 10 | 11 | @SerialName("browser_user_agent") 12 | val userAgent: String, 13 | 14 | @SerialName("client_build_number") 15 | val clientBuildNumber: Int, 16 | 17 | @SerialName("client_version") 18 | val clientBuildVersion: String, 19 | 20 | @SerialName("device") 21 | val deviceName: String, 22 | 23 | @SerialName("os") 24 | val os: String, 25 | 26 | @SerialName("os_sdk_version") 27 | val osSdkVersion: String, 28 | 29 | @SerialName("os_version") 30 | val osVersion: String, 31 | 32 | @SerialName("system_locale") 33 | val systemLocale: String, 34 | 35 | @SerialName("client_performance_cpu") 36 | val cpuPerformance: Int, 37 | 38 | @SerialName("client_performance_memory") 39 | val memoryPerformance: Int, 40 | 41 | @SerialName("cpu_core_count") 42 | val cpuCores: Int, 43 | 44 | @SerialName("accessibility_support_enabled") 45 | val accessibilitySupport: Boolean, 46 | 47 | @SerialName("accessibility_features") 48 | val accessibilityFeatures: Int, 49 | 50 | @SerialName("device_advertiser_id") 51 | val deviceAdId: String, 52 | ) 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/ApiChannel.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ApiChannel( 8 | @SerialName("id") 9 | val id: ApiSnowflake, 10 | 11 | @SerialName("guild_id") 12 | val guildId: ApiSnowflake? = null, 13 | 14 | @SerialName("name") 15 | val name: String, 16 | 17 | @SerialName("type") 18 | val type: Int, 19 | 20 | @SerialName("position") 21 | val position: Int = 0, 22 | 23 | @SerialName("parent_id") 24 | val parentId: ApiSnowflake? = null, 25 | 26 | @SerialName("nsfw") 27 | val nsfw: Boolean = false, 28 | 29 | @SerialName("last_message_id") 30 | val lastMessageId: ApiSnowflake? = null, 31 | ) 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/ApiColor.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.toArgb 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | @JvmInline 9 | value class ApiColor(val internalColor: Int) { 10 | val fullArgbColor get() = internalColor or (0xFF shl 24) 11 | // val rgbColor get() = color and 0xFFFFFF 12 | 13 | // val red: Int get() = (rgbColor shr 16) and 0xFF 14 | // val green: Int get() = (rgbColor shr 8) and 0xFF 15 | // val blue: Int get() = (rgbColor shr 0) and 0xFF 16 | } 17 | 18 | fun Color.toApi(): ApiColor = ApiColor(toArgb()) 19 | 20 | fun ApiColor.toColor(): Color = Color(fullArgbColor) 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/ApiExperiments.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | // Only used for fetching the fingerprint in auth 6 | @Serializable 7 | data class ApiExperiments( 8 | val fingerprint: String, 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/ApiGuild.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ApiGuild( 8 | @SerialName("id") 9 | val id: ApiSnowflake, 10 | 11 | @SerialName("name") 12 | val name: String, 13 | 14 | @SerialName("icon") 15 | val icon: String?, 16 | 17 | @SerialName("banner") 18 | val banner: String? = null, 19 | 20 | @SerialName("premium_tier") 21 | val premiumTier: Int, 22 | 23 | @SerialName("premium_subscription_count") 24 | val premiumSubscriptionCount: Int? = null, 25 | 26 | @SerialName("channels") 27 | val channels: List? = null, 28 | ) 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/ApiGuildMemberChunk.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ApiGuildMemberChunk( 8 | @SerialName("guild_id") 9 | val guildId: ApiSnowflake, 10 | 11 | @SerialName("members") 12 | val members: List, 13 | 14 | @SerialName("chunk_index") 15 | val chunkIndex: Int, 16 | 17 | @SerialName("chunk_count") 18 | val chunkCount: Int, 19 | ) 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/ApiPermissions.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | @JvmInline 7 | value class ApiPermissions(val value: Long) { 8 | constructor(flags: String) : this(flags.toLong()) 9 | 10 | override fun toString(): String = value.toString() 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/GuildMember.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models 2 | 3 | import com.xinto.opencord.rest.models.user.ApiUser 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class ApiGuildMember( 9 | @SerialName("user") 10 | val user: ApiUser?, 11 | 12 | @SerialName("nick") 13 | val nick: String?, 14 | 15 | @SerialName("avatar") 16 | val avatar: String? 17 | ) 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/Snowflake.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | @JvmInline 7 | value class ApiSnowflake(val value: Long) { 8 | constructor(value: String) : this(value.toLong()) 9 | 10 | override fun toString(): String = value.toString() 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/activity/ApiActivityAssets.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models.activity 2 | 3 | import com.xinto.opencord.domain.activity.DomainActivityAssets 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class ApiActivityAssets( 9 | @SerialName("large_image") 10 | val largeImage: String? = null, 11 | 12 | @SerialName("large_text") 13 | val largeText: String? = null, 14 | 15 | @SerialName("small_image") 16 | val smallImage: String? = null, 17 | 18 | @SerialName("small_text") 19 | val smallText: String? = null, 20 | ) 21 | 22 | fun DomainActivityAssets.toApi(): ApiActivityAssets { 23 | return ApiActivityAssets( 24 | largeImage = largeImage, 25 | largeText = largeText, 26 | smallImage = smallImage, 27 | smallText = smallText, 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/activity/ApiActivityButton.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models.activity 2 | 3 | //@Serializable 4 | //data class ApiActivityButton( 5 | // val label: String, 6 | // val url: String, 7 | //) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/activity/ApiActivityMetadata.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models.activity 2 | 3 | import com.xinto.opencord.domain.activity.DomainActivityMetadata 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class ApiActivityMetadata( 9 | @SerialName("album_id") 10 | val albumId: String? = null, 11 | 12 | @SerialName("artist_ids") 13 | val artistIds: List? = null, 14 | 15 | @SerialName("context_uri") 16 | val contextUri: String? = null, 17 | ) 18 | 19 | fun DomainActivityMetadata.toApi(): ApiActivityMetadata { 20 | return ApiActivityMetadata( 21 | albumId = albumId, 22 | artistIds = artistIds, 23 | contextUri = contextUri, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/activity/ApiActivityParty.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models.activity 2 | 3 | import com.xinto.opencord.domain.activity.DomainActivityParty 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ApiActivityParty( 8 | val id: String? = null, 9 | val size: List? = null, 10 | ) 11 | 12 | fun DomainActivityParty.toApi(): ApiActivityParty { 13 | return ApiActivityParty( 14 | id = id, 15 | size = if (currentSize == null || maxSize == null) null else { 16 | listOf(currentSize, maxSize) 17 | }, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/activity/ApiActivitySecrets.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models.activity 2 | 3 | import com.xinto.opencord.domain.activity.DomainActivitySecrets 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ApiActivitySecrets( 8 | val join: String? = null, 9 | val spectate: String? = null, 10 | val match: String? = null, 11 | ) 12 | 13 | fun DomainActivitySecrets.toApi(): ApiActivitySecrets { 14 | return ApiActivitySecrets( 15 | join = join, 16 | spectate = spectate, 17 | match = match, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/activity/ApiActivityTimestamp.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models.activity 2 | 3 | import com.xinto.opencord.domain.activity.DomainActivityTimestamp 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ApiActivityTimestamp( 8 | val start: String? = null, 9 | val end: String? = null, 10 | ) 11 | 12 | fun DomainActivityTimestamp.toApi(): ApiActivityTimestamp { 13 | return ApiActivityTimestamp( 14 | start = start?.toEpochMilliseconds().toString(), 15 | end = end?.toEpochMilliseconds().toString(), 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/embed/ApiEmbedAuthor.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models.embed 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ApiEmbedAuthor( 8 | @SerialName("name") 9 | val name: String, 10 | 11 | @SerialName("url") 12 | val url: String? = null, 13 | 14 | @SerialName("icon_url") 15 | val iconUrl: String? = null, 16 | 17 | @SerialName("proxy_icon_url") 18 | val proxyIconUrl: String? = null, 19 | ) 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/embed/ApiEmbedField.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models.embed 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ApiEmbedField( 8 | @SerialName("name") 9 | val name: String, 10 | 11 | @SerialName("value") 12 | val value: String, 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/embed/ApiEmbedFooter.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models.embed 2 | 3 | import com.xinto.opencord.domain.embed.DomainEmbedFooter 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class ApiEmbedFooter( 9 | @SerialName("text") 10 | val text: String, 11 | 12 | @SerialName("icon_url") 13 | val iconUrl: String? = null, 14 | 15 | @SerialName("proxy_icon_url") 16 | val proxyIconUrl: String? = null, 17 | ) 18 | 19 | fun DomainEmbedFooter.toApi(): ApiEmbedFooter { 20 | return ApiEmbedFooter( 21 | text = text, 22 | iconUrl = iconUrl, 23 | proxyIconUrl = proxyIconUrl, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/embed/ApiEmbedMedia.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models.embed 2 | 3 | import com.xinto.opencord.util.queryParameters 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class ApiEmbedMedia( 9 | @SerialName("url") 10 | val url: String, 11 | 12 | @SerialName("proxy_url") 13 | val proxyUrl: String? = null, 14 | 15 | @SerialName("height") 16 | val height: Int? = null, 17 | 18 | @SerialName("width") 19 | val width: Int? = null, 20 | ) { 21 | /** 22 | * Calculates the proxy url with size or raw url if none 23 | * Only used for images 24 | */ 25 | val sizedUrl: String by lazy { 26 | if (proxyUrl != null) { 27 | val params = queryParameters(2) { 28 | width?.also { append("width", it.toString()) } 29 | height?.also { append("height", it.toString()) } 30 | } 31 | "$proxyUrl$params" 32 | } else { 33 | url 34 | } 35 | } 36 | 37 | val aspectRatio: Float 38 | get() = (width?.toFloat() ?: 1f) / (height?.toFloat() ?: 1f) 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/embed/ApiEmbedProvider.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models.embed 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ApiEmbedProvider( 8 | @SerialName("name") 9 | val name: String? = null, 10 | 11 | @SerialName("url") 12 | val url: String? = null, 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/emoji/ApiEmoji.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models.emoji 2 | 3 | import com.github.materiiapps.partial.Partial 4 | import com.github.materiiapps.partial.missing 5 | import com.github.materiiapps.partial.partial 6 | import com.xinto.opencord.domain.emoji.DomainEmoji 7 | import com.xinto.opencord.domain.emoji.DomainGuildEmoji 8 | import com.xinto.opencord.domain.emoji.DomainUnicodeEmoji 9 | import com.xinto.opencord.domain.emoji.DomainUnknownEmoji 10 | import com.xinto.opencord.rest.models.ApiSnowflake 11 | import kotlinx.serialization.Serializable 12 | 13 | @Serializable 14 | data class ApiEmoji( 15 | val name: String?, 16 | val id: ApiSnowflake?, 17 | val animated: Partial = missing(), 18 | ) 19 | 20 | fun DomainEmoji.toApi(): ApiEmoji? { 21 | return when (this) { 22 | is DomainUnicodeEmoji -> ApiEmoji( 23 | name = emoji, 24 | id = null, 25 | ) 26 | is DomainGuildEmoji -> ApiEmoji( 27 | name = name, 28 | id = ApiSnowflake(id), 29 | animated = if (animated) partial(true) else missing(), 30 | ) 31 | is DomainUnknownEmoji -> null 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/login/ApiLogin.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models.login 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ApiLogin( 8 | @SerialName("token") 9 | val token: String? = null, 10 | 11 | @SerialName("mfa") 12 | val mfa: Boolean = false, 13 | 14 | @SerialName("user_settings") 15 | val userSettings: ApiLoginUserSettings? = null, 16 | 17 | @SerialName("captcha_sitekey") 18 | val captchaSiteKey: String? = null, 19 | 20 | @SerialName("ticket") 21 | val ticket: String? = null, 22 | ) 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/login/ApiLoginUserSettings.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models.login 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ApiLoginUserSettings( 8 | @SerialName("locale") 9 | val locale: String, 10 | 11 | @SerialName("theme") 12 | val theme: String, 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/message/ApiMessageType.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models.message 2 | 3 | import com.github.materiiapps.enumutil.FromValue 4 | import kotlinx.serialization.KSerializer 5 | import kotlinx.serialization.Serializable 6 | import kotlinx.serialization.descriptors.PrimitiveKind 7 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 8 | import kotlinx.serialization.descriptors.SerialDescriptor 9 | import kotlinx.serialization.encoding.Decoder 10 | import kotlinx.serialization.encoding.Encoder 11 | 12 | @FromValue 13 | @Serializable(ApiMessageType.Serializer::class) 14 | enum class ApiMessageType(val value: Int) { 15 | Unknown(-1), 16 | Default(0), 17 | GuildMemberJoin(7), 18 | Reply(19); 19 | 20 | companion object Serializer : KSerializer { 21 | override val descriptor: SerialDescriptor 22 | get() = PrimitiveSerialDescriptor("ApiMessageType", PrimitiveKind.INT) 23 | 24 | override fun deserialize(decoder: Decoder): ApiMessageType { 25 | return fromValue(decoder.decodeInt()) ?: Unknown 26 | } 27 | 28 | override fun serialize(encoder: Encoder, value: ApiMessageType) { 29 | encoder.encodeInt(value.value) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/reaction/ApiReaction.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models.reaction 2 | 3 | import com.xinto.opencord.rest.models.emoji.ApiEmoji 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class ApiReaction( 9 | @SerialName("emoji") 10 | val emoji: ApiEmoji, 11 | 12 | @SerialName("count") 13 | val count: Int, 14 | 15 | @SerialName("me") 16 | val meReacted: Boolean, 17 | ) 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/user/settings/ApiCustomStatus.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models.user.settings 2 | 3 | import com.xinto.opencord.domain.usersettings.DomainCustomStatus 4 | import com.xinto.opencord.rest.models.ApiSnowflake 5 | import kotlinx.serialization.SerialName 6 | import kotlinx.serialization.Serializable 7 | 8 | @Serializable 9 | data class ApiCustomStatus( 10 | @SerialName("text") 11 | val text: String?, 12 | 13 | @SerialName("expires_at") 14 | val expiresAt: String?, 15 | 16 | @SerialName("emoji_id") 17 | val emojiId: ApiSnowflake?, 18 | 19 | @SerialName("emoji_name") 20 | val emojiName: String? 21 | ) 22 | 23 | fun DomainCustomStatus.toApi(): ApiCustomStatus { 24 | return ApiCustomStatus( 25 | text = text, 26 | expiresAt = null, // TODO: make timestamp serializer 27 | emojiId = emojiId?.let { ApiSnowflake(it) }, 28 | emojiName = emojiName, 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/user/settings/ApiFriendSources.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models.user.settings 2 | 3 | import com.xinto.opencord.domain.usersettings.DomainFriendSources 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class ApiFriendSources( 9 | @SerialName("all") 10 | val all: Boolean? = null, 11 | 12 | @SerialName("mutual_friends") 13 | val mutualFriends: Boolean? = null, 14 | 15 | @SerialName("mutual_guilds") 16 | val mutualGuilds: Boolean? = null, 17 | ) 18 | 19 | fun DomainFriendSources.toApi(): ApiFriendSources { 20 | return ApiFriendSources( 21 | all = all, 22 | mutualFriends = mutualFriends, 23 | mutualGuilds = mutualGuilds, 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/models/user/settings/ApiGuildFolder.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.models.user.settings 2 | 3 | import com.xinto.opencord.domain.usersettings.DomainGuildFolder 4 | import com.xinto.opencord.rest.models.ApiSnowflake 5 | import kotlinx.serialization.SerialName 6 | import kotlinx.serialization.Serializable 7 | 8 | @Serializable 9 | data class ApiGuildFolder( 10 | @SerialName("guild_ids") 11 | val guildIds: List, 12 | 13 | @SerialName("id") 14 | val id: ApiSnowflake? = null, 15 | 16 | @SerialName("name") 17 | val name: String? = null, 18 | ) 19 | 20 | fun DomainGuildFolder.toApi(): ApiGuildFolder { 21 | return ApiGuildFolder( 22 | id = id?.let { ApiSnowflake(it) }, 23 | guildIds = guildIds.map { ApiSnowflake(it) }, 24 | name = name, 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/rest/service/DiscordCdnService.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.rest.service 2 | 3 | import com.xinto.opencord.BuildConfig 4 | 5 | interface DiscordCdnService 6 | 7 | class DiscordCdnServiceImpl : DiscordCdnService { 8 | companion object { 9 | private const val BASE = BuildConfig.URL_CDN 10 | 11 | fun getDefaultAvatarUrl(avatar: Int): String { 12 | return "$BASE/embed/avatars/$avatar.png" 13 | } 14 | 15 | fun getUserAvatarUrl(userId: Long, avatarHash: String): String { 16 | return "$BASE/avatars/${userId}/$avatarHash.webp?size=100" 17 | } 18 | 19 | fun getGuildIconUrl(guildId: Long, iconHash: String): String { 20 | return "$BASE/icons/$guildId/$iconHash.webp?size=128" 21 | } 22 | 23 | fun getGuildBannerUrl(guildId: Long, iconHash: String): String { 24 | return "$BASE/banners/$guildId/$iconHash.webp?size=1024" 25 | } 26 | 27 | fun getGuildEmojiUrl(emojiId: Long, animated: Boolean): String { 28 | val ext = if (animated) "gif" else "webp" 29 | return "$BASE/emojis/$emojiId.$ext?size=64&quality=lossless" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/store/Event.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("NOTHING_TO_INLINE") 2 | 3 | package com.xinto.opencord.store 4 | 5 | sealed interface Event { 6 | data class Add(val data: A) : Event 7 | data class Update(val data: U) : Event 8 | data class Delete(val data: D) : Event 9 | 10 | @Suppress("FunctionName") 11 | companion object { 12 | inline fun Add(data: A) = Event.Add(data) 13 | inline fun Update(data: U) = Event.Update(data) 14 | inline fun Delete(data: D) = Event.Delete(data) 15 | } 16 | } 17 | 18 | inline fun Event.fold( 19 | onAdd: (A) -> R, 20 | onUpdate: (U) -> R, 21 | onDelete: (D) -> R, 22 | ): R { 23 | return when (this) { 24 | is Event.Add -> onAdd.invoke(data) 25 | is Event.Update -> onUpdate.invoke(data) 26 | is Event.Delete -> onDelete.invoke(data) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/SplashScreenActivity.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 7 | import com.xinto.opencord.manager.AccountManager 8 | import com.xinto.opencord.manager.ActivityManager 9 | import org.koin.android.ext.android.inject 10 | 11 | @SuppressLint("CustomSplashScreen") 12 | class SplashScreenActivity : ComponentActivity() { 13 | private val accountManager: AccountManager by inject() 14 | private val activityManager: ActivityManager by inject() 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | installSplashScreen() 18 | super.onCreate(savedInstanceState) 19 | 20 | val activity = if (accountManager.isLoggedIn) AppActivity::class else LoginActivity::class 21 | 22 | finishAndRemoveTask() 23 | activityManager.startActivity(activity) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/components/OCBadgeBox.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.shape.CircleShape 7 | import androidx.compose.material3.Surface 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Shape 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | fun OCBadgeBox( 16 | modifier: Modifier = Modifier, 17 | badge: (@Composable () -> Unit)?, 18 | badgeShape: Shape = CircleShape, 19 | badgeAlignment: Alignment = Alignment.BottomEnd, 20 | badgePadding: PaddingValues = PaddingValues(), 21 | content: @Composable () -> Unit, 22 | ) { 23 | Box( 24 | contentAlignment = Alignment.Center, 25 | modifier = modifier, 26 | ) { 27 | content() 28 | if (badge != null) { 29 | Surface( 30 | shape = badgeShape, 31 | modifier = Modifier 32 | .padding(badgePadding) 33 | .align(badgeAlignment), 34 | ) { 35 | Box( 36 | modifier = Modifier.padding(3.dp), 37 | ) { 38 | badge() 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/components/attachment/AttachmentPicture.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.components.attachment 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.draw.clip 7 | import com.xinto.opencord.ui.components.OCImage 8 | import com.xinto.opencord.ui.components.OCSize 9 | 10 | @Composable 11 | fun AttachmentPicture( 12 | url: String, 13 | width: Int, 14 | height: Int, 15 | modifier: Modifier = Modifier, 16 | ) { 17 | OCImage( 18 | url = url, 19 | size = OCSize(width, height), 20 | modifier = modifier 21 | .clip(MaterialTheme.shapes.small), 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/components/captcha/HCaptcha.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.components.captcha 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.DisposableEffect 5 | import androidx.compose.ui.platform.LocalContext 6 | import com.hcaptcha.sdk.HCaptcha 7 | import com.hcaptcha.sdk.HCaptchaError 8 | import org.koin.compose.koinInject 9 | import org.koin.core.parameter.parametersOf 10 | 11 | @Composable 12 | fun HCaptcha( 13 | onSuccess: (token: String) -> Unit, 14 | onFailure: (HCaptchaError, code: Int) -> Unit, 15 | ) { 16 | val context = LocalContext.current 17 | val captcha: HCaptcha = koinInject { parametersOf(context) } 18 | DisposableEffect(captcha) { 19 | captcha.verifyWithHCaptcha() 20 | .addOnSuccessListener { 21 | onSuccess(it.tokenResult) 22 | } 23 | .addOnFailureListener { 24 | onFailure(it.hCaptchaError, it.statusCode) 25 | } 26 | onDispose {} 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/components/channel/list/ChannelListCategoryItem.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.components.channel.list 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.material3.LocalTextStyle 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.CompositionLocalProvider 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.clip 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | fun ChannelListCategoryItem( 16 | modifier: Modifier = Modifier, 17 | title: (@Composable () -> Unit), 18 | icon: (@Composable () -> Unit), 19 | onClick: () -> Unit, 20 | ) { 21 | Box( 22 | modifier = modifier 23 | .clip(MaterialTheme.shapes.small) 24 | .clickable(onClick = onClick), 25 | ) { 26 | Row( 27 | horizontalArrangement = Arrangement.spacedBy(4.dp), 28 | modifier = Modifier 29 | .fillMaxWidth() 30 | .padding(4.dp), 31 | verticalAlignment = Alignment.CenterVertically, 32 | ) { 33 | CompositionLocalProvider( 34 | LocalTextStyle provides MaterialTheme.typography.labelMedium, 35 | ) { 36 | Box( 37 | modifier = Modifier.size(18.dp), 38 | contentAlignment = Alignment.Center, 39 | ) { 40 | icon() 41 | } 42 | title() 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/components/embed/EmbedField.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.components.embed 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.ProvideTextStyle 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Composable 13 | fun EmbedField( 14 | name: String, 15 | value: String, 16 | modifier: Modifier = Modifier, 17 | ) { 18 | Column( 19 | modifier = modifier, 20 | verticalArrangement = Arrangement.spacedBy(4.dp), 21 | ) { 22 | ProvideTextStyle(MaterialTheme.typography.labelMedium) { 23 | Text(name) 24 | } 25 | ProvideTextStyle(MaterialTheme.typography.bodySmall) { 26 | Text(value) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/components/embed/EmbedFooter.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.components.embed 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.foundation.shape.CircleShape 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.ProvideTextStyle 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.clip 14 | import androidx.compose.ui.unit.dp 15 | import com.xinto.opencord.ui.components.OCImage 16 | import com.xinto.opencord.ui.components.OCSize 17 | 18 | @Composable 19 | fun EmbedFooter( 20 | text: String, 21 | iconUrl: String?, 22 | timestamp: String?, 23 | modifier: Modifier = Modifier, 24 | ) { 25 | Row( 26 | modifier = modifier, 27 | horizontalArrangement = Arrangement.spacedBy(4.dp), 28 | verticalAlignment = Alignment.CenterVertically, 29 | ) { 30 | if (iconUrl != null) { 31 | OCImage( 32 | url = iconUrl, 33 | size = OCSize(64, 64), 34 | modifier = Modifier 35 | .size(24.dp) 36 | .clip(CircleShape), 37 | ) 38 | } 39 | 40 | ProvideTextStyle(MaterialTheme.typography.labelSmall) { 41 | Text(text) 42 | 43 | if (timestamp != null) { 44 | Text("•") 45 | Text(timestamp) 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/components/embed/SpotifyEmbed.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.components.embed 2 | 3 | import android.annotation.SuppressLint 4 | import android.graphics.Color 5 | import android.webkit.WebSettings 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.unit.dp 10 | import com.google.accompanist.web.WebView 11 | import com.google.accompanist.web.rememberWebViewState 12 | import com.xinto.opencord.ui.util.ScrollableWebView 13 | 14 | @Composable 15 | fun SpotifyEmbed( 16 | embedUrl: String, 17 | isSpotifyTrack: Boolean, 18 | ) { 19 | WebView( 20 | state = rememberWebViewState(embedUrl), 21 | captureBackPresses = false, 22 | onCreated = { webView -> 23 | webView.setBackgroundColor(Color.TRANSPARENT) 24 | webView.settings.apply { 25 | @SuppressLint("SetJavaScriptEnabled") 26 | javaScriptEnabled = true 27 | allowFileAccess = false 28 | mediaPlaybackRequiresUserGesture = false 29 | cacheMode = WebSettings.LOAD_NO_CACHE 30 | setGeolocationEnabled(false) 31 | } 32 | }, 33 | factory = { ScrollableWebView(it) }, 34 | modifier = Modifier 35 | .height(if (isSpotifyTrack) 80.dp else 500.dp), 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/components/guild/list/GuildsListHeaderItem.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.components.guild.list 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.res.painterResource 11 | import androidx.compose.ui.res.stringResource 12 | import androidx.compose.ui.unit.dp 13 | import com.xinto.opencord.R 14 | 15 | @Composable 16 | fun GuildsListHeaderItem( 17 | modifier: Modifier = Modifier, 18 | ) { 19 | Box( 20 | contentAlignment = Alignment.Center, 21 | modifier = modifier.size(48.dp), 22 | ) { 23 | Icon( 24 | modifier = Modifier 25 | .size(35.dp), 26 | tint = MaterialTheme.colorScheme.primary, 27 | painter = painterResource(R.drawable.ic_discord_logo), 28 | contentDescription = stringResource(R.string.guilds_home), 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/components/guild/list/GuildsListItemImage.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.components.guild.list 2 | 3 | import androidx.compose.foundation.layout.size 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.unit.dp 7 | import com.xinto.opencord.ui.components.OCImage 8 | 9 | @Composable 10 | fun GuildsListItemImage( 11 | url: String, 12 | modifier: Modifier = Modifier, 13 | ) { 14 | OCImage( 15 | url = url, 16 | memoryCaching = true, 17 | modifier = modifier 18 | .size(48.dp), 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/components/indicator/UnreadIndicator.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.components.indicator 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.width 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.draw.clip 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.unit.dp 12 | 13 | @Composable 14 | fun UnreadIndicator( 15 | modifier: Modifier = Modifier, 16 | ) { 17 | Box( 18 | modifier = modifier 19 | .clip( 20 | RoundedCornerShape( 21 | topEnd = 12.dp, 22 | bottomEnd = 12.dp, 23 | ), 24 | ) 25 | .background(Color.White) 26 | .width(4.dp), 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/components/message/MessageAvatar.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.components.message 2 | 3 | import androidx.compose.foundation.shape.CircleShape 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.draw.clip 7 | import com.xinto.opencord.ui.components.OCImage 8 | import com.xinto.opencord.ui.components.OCSize 9 | 10 | @Composable 11 | fun MessageAvatar( 12 | url: String, 13 | modifier: Modifier = Modifier, 14 | ) { 15 | OCImage( 16 | url = url, 17 | memoryCaching = true, 18 | size = OCSize(100, 100), 19 | modifier = modifier 20 | .clip(CircleShape), 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/components/message/MessageContent.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.components.message 2 | 3 | import androidx.compose.material3.Text 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.text.AnnotatedString 7 | import com.xinto.opencord.ui.util.messageInlineContent 8 | 9 | @Composable 10 | fun MessageContent( 11 | text: AnnotatedString, 12 | modifier: Modifier = Modifier, 13 | ) { 14 | Text( 15 | modifier = modifier, 16 | text = text, 17 | inlineContent = messageInlineContent(), 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/components/message/reply/MessageReferenced.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.components.message.reply 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.ProvideTextStyle 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | fun MessageReferenced( 16 | modifier: Modifier = Modifier, 17 | avatar: @Composable (() -> Unit)? = null, 18 | author: @Composable (() -> Unit)? = null, 19 | content: @Composable (() -> Unit)? = null, 20 | ) { 21 | Row( 22 | modifier = modifier, 23 | horizontalArrangement = Arrangement.spacedBy(4.dp), 24 | verticalAlignment = Alignment.CenterVertically, 25 | ) { 26 | if (avatar != null) { 27 | Box( 28 | modifier = Modifier.size(20.dp), 29 | contentAlignment = Alignment.Center, 30 | ) { 31 | avatar() 32 | } 33 | } 34 | if (author != null) { 35 | ProvideTextStyle(MaterialTheme.typography.labelMedium) { 36 | author() 37 | } 38 | } 39 | if (content != null) { 40 | ProvideTextStyle(MaterialTheme.typography.bodySmall) { 41 | content() 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/components/message/reply/MessageReferencedAuthor.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.components.message.reply 2 | 3 | import androidx.compose.material3.Text 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.text.style.TextOverflow 7 | 8 | @Composable 9 | fun MessageReferencedAuthor( 10 | author: String, 11 | modifier: Modifier = Modifier 12 | ) { 13 | Text( 14 | modifier = modifier, 15 | text = author, 16 | overflow = TextOverflow.Ellipsis, 17 | maxLines = 1, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/components/message/reply/MessageReferencedContent.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.components.message.reply 2 | 3 | import androidx.compose.material3.Text 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.text.AnnotatedString 7 | import androidx.compose.ui.text.style.TextOverflow 8 | import com.xinto.opencord.ui.util.messageInlineContent 9 | 10 | @Composable 11 | fun MessageReferencedContent( 12 | text: AnnotatedString, 13 | modifier: Modifier = Modifier, 14 | ) { 15 | Text( 16 | modifier = modifier, 17 | text = text, 18 | inlineContent = messageInlineContent(), 19 | overflow = TextOverflow.Ellipsis, 20 | maxLines = 1, 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/components/message/reply/MessageReplyBranch.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.components.message.reply 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.Color 8 | import androidx.compose.ui.graphics.Path 9 | import androidx.compose.ui.graphics.drawscope.Stroke 10 | 11 | @Composable 12 | fun MessageReplyBranch( 13 | modifier: Modifier = Modifier, 14 | color: Color = MaterialTheme.colorScheme.outline 15 | ) { 16 | Canvas(modifier = modifier) { 17 | drawPath( 18 | path = Path().apply { 19 | moveTo( 20 | x = 0f, 21 | y = size.height, 22 | ) 23 | cubicTo( 24 | x1 = 0f, 25 | y1 = 0f, 26 | x2 = 0f, 27 | y2 = 0f, 28 | x3 = size.width, 29 | y3 = 0f, 30 | ) 31 | }, 32 | color = color, 33 | style = Stroke( 34 | width = 5f, 35 | ), 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/navigation/AppDestination.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.navigation 2 | 3 | import android.app.Activity 4 | import android.os.Parcelable 5 | import androidx.compose.runtime.Immutable 6 | import com.xinto.opencord.ui.util.topDestination 7 | import dev.olshevski.navigation.reimagined.NavController 8 | import dev.olshevski.navigation.reimagined.pop 9 | import dev.olshevski.navigation.reimagined.replaceAll 10 | import kotlinx.parcelize.Parcelize 11 | 12 | @Parcelize 13 | sealed interface AppDestination : Parcelable { 14 | @Parcelize 15 | object Main : AppDestination 16 | 17 | @Parcelize 18 | object Settings : AppDestination 19 | 20 | @Parcelize 21 | object Mentions : AppDestination 22 | 23 | @Parcelize 24 | data class Pins(val data: PinsScreenData) : AppDestination 25 | } 26 | 27 | @Parcelize 28 | @Immutable 29 | data class PinsScreenData( 30 | val channelId: Long, 31 | ) : Parcelable 32 | 33 | context(Activity) 34 | fun NavController.back() { 35 | val top = topDestination() 36 | 37 | if (top == AppDestination.Main) { 38 | finish() 39 | } else if (backstack.entries.size > 1) { 40 | pop() 41 | } else { 42 | replaceAll(AppDestination.Main) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/navigation/LoginDestination.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.navigation 2 | 3 | import android.app.Activity 4 | import android.os.Parcelable 5 | import com.xinto.opencord.ui.util.topDestination 6 | import dev.olshevski.navigation.reimagined.NavController 7 | import dev.olshevski.navigation.reimagined.pop 8 | import dev.olshevski.navigation.reimagined.replaceAll 9 | import kotlinx.parcelize.Parcelize 10 | 11 | @Parcelize 12 | sealed interface LoginDestination : Parcelable { 13 | @Parcelize 14 | object Login : LoginDestination 15 | 16 | @Parcelize 17 | object Landing : LoginDestination 18 | } 19 | 20 | context(Activity) 21 | fun NavController.back() { 22 | val top = topDestination() 23 | 24 | if (top == LoginDestination.Landing) { 25 | finish() 26 | } else if (backstack.entries.size > 1) { 27 | pop() 28 | } else { 29 | replaceAll(LoginDestination.Landing) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/screens/SettingsScreen.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.screens 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.lazy.LazyColumn 6 | import androidx.compose.material3.* 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.res.painterResource 10 | import androidx.compose.ui.res.stringResource 11 | import com.xinto.opencord.R 12 | 13 | @Composable 14 | fun Settings( 15 | onBackClick: () -> Unit, 16 | modifier: Modifier = Modifier, 17 | ) { 18 | Scaffold( 19 | modifier = modifier, 20 | topBar = { 21 | TopAppBar( 22 | title = { Text(stringResource(R.string.settings_title)) }, 23 | navigationIcon = { 24 | IconButton(onClick = onBackClick) { 25 | Icon( 26 | painter = painterResource(R.drawable.ic_arrow_back), 27 | contentDescription = null, 28 | ) 29 | } 30 | }, 31 | ) 32 | }, 33 | ) { paddingValues -> 34 | LazyColumn( 35 | modifier = Modifier 36 | .fillMaxSize() 37 | .padding(paddingValues), 38 | ) { 39 | 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/screens/home/panels/channel/ChannelsListUnselected.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.screens.home.panels.channel 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.ProvideTextStyle 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.res.stringResource 11 | import com.xinto.opencord.R 12 | 13 | @Composable 14 | fun ChannelsListUnselected( 15 | modifier: Modifier = Modifier, 16 | ) { 17 | Box( 18 | modifier = modifier, 19 | contentAlignment = Alignment.Center, 20 | ) { 21 | ProvideTextStyle(MaterialTheme.typography.titleMedium) { 22 | Text(stringResource(R.string.channel_unselected_message)) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/screens/home/panels/chat/ChatError.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.screens.home.panels.chat 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.material3.* 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.CompositionLocalProvider 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.res.painterResource 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.text.style.TextAlign 14 | import androidx.compose.ui.unit.dp 15 | import com.xinto.opencord.R 16 | 17 | @Composable 18 | fun ChatError( 19 | modifier: Modifier = Modifier, 20 | ) { 21 | Column( 22 | modifier = modifier, 23 | horizontalAlignment = Alignment.CenterHorizontally, 24 | verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), 25 | ) { 26 | CompositionLocalProvider( 27 | LocalContentColor provides MaterialTheme.colorScheme.error, 28 | LocalTextStyle provides MaterialTheme.typography.titleMedium, 29 | ) { 30 | Icon( 31 | modifier = Modifier.size(56.dp), 32 | painter = painterResource(R.drawable.ic_error), 33 | contentDescription = null, 34 | ) 35 | Text( 36 | text = stringResource(R.string.chat_loading_error), 37 | textAlign = TextAlign.Center, 38 | ) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/screens/home/panels/chat/ChatUnselected.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.screens.home.panels.chat 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.ProvideTextStyle 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.res.stringResource 11 | import com.xinto.opencord.R 12 | 13 | @Composable 14 | fun ChatUnselected( 15 | modifier: Modifier = Modifier, 16 | ) { 17 | Box( 18 | modifier = modifier, 19 | contentAlignment = Alignment.Center, 20 | ) { 21 | ProvideTextStyle(MaterialTheme.typography.titleMedium) { 22 | Text(stringResource(R.string.chat_unselected_message)) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/screens/home/panels/guild/GuildsList.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.screens.home.panels.guild 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import com.xinto.opencord.ui.viewmodel.GuildsViewModel 6 | import org.koin.androidx.compose.getViewModel 7 | 8 | @Composable 9 | fun GuildsList( 10 | onGuildSelect: () -> Unit, 11 | modifier: Modifier = Modifier, 12 | viewModel: GuildsViewModel = getViewModel(), 13 | ) { 14 | when (viewModel.state) { 15 | GuildsViewModel.State.Loading -> { 16 | GuildsListLoading(modifier = modifier) 17 | } 18 | GuildsViewModel.State.Loaded -> { 19 | GuildsListLoaded( 20 | items = viewModel.listItems, 21 | modifier = modifier, 22 | onGuildSelect = { 23 | viewModel.selectGuild(it) 24 | onGuildSelect() 25 | }, 26 | ) 27 | } 28 | GuildsViewModel.State.Error -> { 29 | 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/screens/home/panels/member/MembersList.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.screens.home.panels.member 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.Surface 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.unit.dp 12 | 13 | @Composable 14 | fun MembersList( 15 | modifier: Modifier, 16 | ) { 17 | Surface( 18 | shape = MaterialTheme.shapes.medium, 19 | tonalElevation = 1.dp, 20 | modifier = modifier, 21 | ) { 22 | Box( 23 | modifier = Modifier.fillMaxSize(), 24 | contentAlignment = Alignment.Center, 25 | ) { 26 | Text( 27 | text = "Members List (soon)", 28 | style = MaterialTheme.typography.bodyMedium, 29 | ) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/screens/pins/PinsScreenError.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.screens.pins 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.size 6 | import androidx.compose.material3.* 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.CompositionLocalProvider 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.res.painterResource 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.text.style.TextAlign 14 | import androidx.compose.ui.unit.dp 15 | import com.xinto.opencord.R 16 | 17 | @Composable 18 | fun PinsScreenError( 19 | modifier: Modifier = Modifier, 20 | ) { 21 | Column( 22 | modifier = modifier, 23 | horizontalAlignment = Alignment.CenterHorizontally, 24 | verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), 25 | ) { 26 | CompositionLocalProvider( 27 | LocalContentColor provides MaterialTheme.colorScheme.error, 28 | LocalTextStyle provides MaterialTheme.typography.titleMedium, 29 | ) { 30 | Icon( 31 | modifier = Modifier.size(56.dp), 32 | painter = painterResource(R.drawable.ic_error), 33 | contentDescription = null, 34 | ) 35 | Text( 36 | text = stringResource(R.string.pins_loading_error), 37 | textAlign = TextAlign.Center, 38 | ) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/screens/pins/PinsScreenLoading.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.screens.pins 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.material3.CircularProgressIndicator 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Alignment 7 | import androidx.compose.ui.Modifier 8 | 9 | @Composable 10 | fun PinsScreenLoading( 11 | modifier: Modifier = Modifier 12 | ) { 13 | Box( 14 | modifier = modifier, 15 | contentAlignment = Alignment.Center, 16 | ) { 17 | CircularProgressIndicator() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.theme 2 | 3 | import androidx.compose.material3.Shapes 4 | 5 | val Shapes = Shapes() 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/util/CompositePaddingValues.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.util 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.ui.unit.Dp 5 | import androidx.compose.ui.unit.LayoutDirection 6 | import androidx.compose.ui.unit.dp 7 | 8 | class CompositePaddingValues(private vararg val items: PaddingValues) : PaddingValues { 9 | override fun calculateBottomPadding(): Dp { 10 | return items.fold(0.dp) { acc, paddingValues -> 11 | acc + paddingValues.calculateBottomPadding() 12 | } 13 | } 14 | 15 | override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp { 16 | return items.fold(0.dp) { acc, paddingValues -> 17 | acc + paddingValues.calculateLeftPadding(layoutDirection) 18 | } 19 | } 20 | 21 | override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp { 22 | return items.fold(0.dp) { acc, paddingValues -> 23 | acc + paddingValues.calculateRightPadding(layoutDirection) 24 | } 25 | } 26 | 27 | override fun calculateTopPadding(): Dp { 28 | return items.fold(0.dp) { acc, paddingValues -> 29 | acc + paddingValues.calculateTopPadding() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/util/ContentAlpha.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.util 2 | 3 | import androidx.compose.material3.LocalContentColor 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.CompositionLocalProvider 6 | import androidx.compose.runtime.ReadOnlyComposable 7 | 8 | object ContentAlpha { 9 | val full: Float 10 | @Composable 11 | @ReadOnlyComposable 12 | get() = 1.0f 13 | 14 | val extraHigh: Float 15 | @Composable 16 | @ReadOnlyComposable 17 | get() = 0.90f 18 | 19 | val veryHigh: Float 20 | @Composable 21 | @ReadOnlyComposable 22 | get() = 0.80f 23 | 24 | val high: Float 25 | @Composable 26 | @ReadOnlyComposable 27 | get() = 0.70f 28 | 29 | val medium: Float 30 | @Composable 31 | @ReadOnlyComposable 32 | get() = 0.60f 33 | 34 | val low: Float 35 | @Composable 36 | @ReadOnlyComposable 37 | get() = 0.50f 38 | 39 | val veryLow: Float 40 | @Composable 41 | @ReadOnlyComposable 42 | get() = 0.40f 43 | 44 | val extraLow: Float 45 | @Composable 46 | @ReadOnlyComposable 47 | get() = 0.30f 48 | } 49 | 50 | @Composable 51 | fun ProvideContentAlpha(value: Float, content: @Composable () -> Unit) { 52 | val opaqueColor = LocalContentColor.current.copy(alpha = value) 53 | CompositionLocalProvider( 54 | LocalContentColor provides opaqueColor, 55 | content = content, 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/util/CornerBasedShape.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.util 2 | 3 | import androidx.compose.foundation.shape.CornerBasedShape 4 | import androidx.compose.foundation.shape.CornerSize 5 | import androidx.compose.ui.geometry.Size 6 | import androidx.compose.ui.graphics.Outline 7 | import androidx.compose.ui.graphics.Path 8 | import androidx.compose.ui.unit.LayoutDirection 9 | 10 | object EmptyCornerBasedShape : CornerBasedShape( 11 | topStart = CornerSize(0f), 12 | topEnd = CornerSize(0f), 13 | bottomStart = CornerSize(0f), 14 | bottomEnd = CornerSize(0f), 15 | ) { 16 | override fun copy( 17 | topStart: CornerSize, 18 | topEnd: CornerSize, 19 | bottomEnd: CornerSize, 20 | bottomStart: CornerSize 21 | ): CornerBasedShape { 22 | return this 23 | } 24 | 25 | override fun createOutline( 26 | size: Size, 27 | topStart: Float, 28 | topEnd: Float, 29 | bottomEnd: Float, 30 | bottomStart: Float, 31 | layoutDirection: LayoutDirection 32 | ): Outline { 33 | return Outline.Generic(Path()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/util/LocalViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.util 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.DisposableEffect 5 | import androidx.compose.runtime.remember 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.ViewModelStore 8 | import androidx.lifecycle.ViewModelStoreOwner 9 | import org.koin.androidx.compose.getViewModel 10 | import org.koin.core.annotation.KoinInternalApi 11 | import org.koin.core.context.GlobalContext 12 | import org.koin.core.parameter.ParametersDefinition 13 | import org.koin.core.qualifier.Qualifier 14 | import org.koin.core.scope.Scope 15 | 16 | /** 17 | * Creates and manages a ViewModel bound to the current composable scope. 18 | * Once the composable goes out of scope, the ViewModel is destroyed. 19 | */ 20 | @OptIn(KoinInternalApi::class) 21 | @Composable 22 | inline fun getLocalViewModel( 23 | qualifier: Qualifier? = null, 24 | scope: Scope = GlobalContext.get().scopeRegistry.rootScope, 25 | noinline parameters: ParametersDefinition? = null, 26 | ): T { 27 | val owner = remember { 28 | object : ViewModelStoreOwner { 29 | override val viewModelStore = ViewModelStore() 30 | } 31 | } 32 | 33 | DisposableEffect(owner) { 34 | onDispose { 35 | owner.viewModelStore.clear() 36 | } 37 | } 38 | 39 | return getViewModel( 40 | qualifier = qualifier, 41 | viewModelStoreOwner = owner, 42 | scope = scope, 43 | parameters = parameters, 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/util/MessageInlineContent.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.util 2 | 3 | import androidx.compose.foundation.text.InlineTextContent 4 | import androidx.compose.material3.LocalTextStyle 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.ui.text.Placeholder 8 | import androidx.compose.ui.text.PlaceholderVerticalAlign 9 | import androidx.compose.ui.unit.sp 10 | import com.xinto.opencord.BuildConfig 11 | import com.xinto.opencord.ui.components.OCImage 12 | import kotlinx.collections.immutable.ImmutableMap 13 | import kotlinx.collections.immutable.persistentHashMapOf 14 | 15 | @Composable 16 | fun messageInlineContent(): ImmutableMap { 17 | val emoteSize = (LocalTextStyle.current.fontSize.value + 2f).sp 18 | 19 | return remember(emoteSize) { 20 | persistentHashMapOf( 21 | "emote" to InlineTextContent( 22 | placeholder = Placeholder( 23 | width = emoteSize, 24 | height = emoteSize, 25 | placeholderVerticalAlign = PlaceholderVerticalAlign.Center, 26 | ), 27 | ) { emoteId -> 28 | OCImage( 29 | url = "${BuildConfig.URL_CDN}/emojis/$emoteId", 30 | memoryCaching = true, 31 | ) 32 | }, 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/util/NavUtil.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.util 2 | 3 | import dev.olshevski.navigation.reimagined.NavController 4 | 5 | fun NavController.topDestination() = 6 | backstack.entries.lastOrNull()?.destination 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/util/ScrollableWebView.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.util 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.view.MotionEvent 6 | import android.webkit.WebView 7 | 8 | class ScrollableWebView(context: Context) : WebView(context) { 9 | @SuppressLint("ClickableViewAccessibility") 10 | override fun onTouchEvent(event: MotionEvent?): Boolean { 11 | requestDisallowInterceptTouchEvent(true) 12 | return super.onTouchEvent(event) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/util/UnsafeImmutableList.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.util 2 | 3 | import androidx.compose.runtime.Immutable 4 | import kotlinx.collections.immutable.ImmutableList 5 | 6 | /** 7 | * A compose-stable wrapper over a list for performance. 8 | * 9 | * This does NOT guarantee stability. It is merely a stable wrapper over another list, 10 | * and assumes the reader knows that it shouldn't change through crucial parts of rendering. 11 | */ 12 | @Immutable 13 | class UnsafeImmutableList( 14 | private val items: List, 15 | ) : ImmutableList { 16 | override val size = items.size 17 | 18 | override fun get(index: Int) = items[index] 19 | override fun isEmpty() = items.isEmpty() 20 | override fun iterator() = items.iterator() 21 | override fun listIterator() = items.listIterator() 22 | override fun listIterator(index: Int) = items.listIterator(index) 23 | override fun lastIndexOf(element: T) = items.lastIndexOf(element) 24 | override fun indexOf(element: T) = items.indexOf(element) 25 | override fun containsAll(elements: Collection) = items.containsAll(elements) 26 | override fun contains(element: T) = items.contains(element) 27 | } 28 | 29 | fun List.toUnsafeImmutableList(): ImmutableList = 30 | UnsafeImmutableList(this) 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/util/VoidablePaddingValues.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.util 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.ui.unit.Dp 5 | import androidx.compose.ui.unit.LayoutDirection 6 | import androidx.compose.ui.unit.dp 7 | 8 | /** 9 | * Disable any (top/bottom/left/right) padding for [PaddingValues], 10 | * without touching the rest of the values. 11 | */ 12 | class VoidablePaddingValues( 13 | private val existing: PaddingValues, 14 | private val top: Boolean = true, 15 | private val bottom: Boolean = true, 16 | private val left: Boolean = true, 17 | private val right: Boolean = true, 18 | ) : PaddingValues { 19 | override fun calculateBottomPadding(): Dp { 20 | return if (!bottom) 0.dp else { 21 | existing.calculateBottomPadding() 22 | } 23 | } 24 | 25 | override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp { 26 | return if (!left) 0.dp else { 27 | existing.calculateLeftPadding(layoutDirection) 28 | } 29 | } 30 | 31 | override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp { 32 | return if (!right) 0.dp else { 33 | existing.calculateRightPadding(layoutDirection) 34 | } 35 | } 36 | 37 | override fun calculateTopPadding(): Dp { 38 | return if (!top) 0.dp else { 39 | existing.calculateTopPadding() 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/ui/viewmodel/base/BasePersistenceViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.ui.viewmodel.base 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.xinto.opencord.manager.PersistentDataManager 5 | 6 | abstract class BasePersistenceViewModel( 7 | private val persistentDataManager: PersistentDataManager 8 | ) : ViewModel() { 9 | // TODO: migrate this to a store 10 | 11 | protected var persistentGuildId 12 | get() = persistentDataManager.persistentGuildId 13 | set(value) { 14 | persistentDataManager.persistentGuildId = value 15 | } 16 | 17 | protected var persistentChannelId 18 | get() = persistentDataManager.persistentChannelId 19 | set(value) { 20 | persistentDataManager.persistentChannelId = value 21 | } 22 | 23 | protected var persistentCollapsedCategories 24 | get() = persistentDataManager.collapsedCategories 25 | set(value) { 26 | persistentDataManager.collapsedCategories = value 27 | } 28 | 29 | protected fun addPersistentCollapseCategory(id: Long) { 30 | persistentDataManager.addCollapsedCategory(id) 31 | } 32 | 33 | protected fun removePersistentCollapseCategory(id: Long) { 34 | persistentDataManager.removeCollapsedCategory(id) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/util/FlowUtil.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.util 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.Job 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.FlowCollector 7 | import kotlinx.coroutines.launch 8 | 9 | fun Flow.collectIn(scope: CoroutineScope, collector: FlowCollector): Job { 10 | return scope.launch { 11 | this@collectIn.collect(collector) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/util/KtorUtil.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.util 2 | 3 | import io.ktor.http.* 4 | 5 | fun queryParameters(internalSize: Int = 4, block: ParametersBuilder.() -> Unit): String { 6 | val builder = ParametersBuilder(internalSize) 7 | block(builder) 8 | 9 | return if (builder.isEmpty()) { 10 | "" 11 | } else { 12 | "?${builder.build().formUrlEncode()}" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/util/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.util 2 | 3 | import android.util.Log 4 | import com.xinto.opencord.BuildConfig 5 | 6 | interface Logger { 7 | fun error(tag: String, message: String?, throwable: Throwable?) 8 | fun warn(tag: String, message: String, throwable: Throwable?) 9 | fun info(tag: String, message: String) 10 | fun debug(tag: String, message: String) 11 | } 12 | 13 | class LoggerImpl : Logger { 14 | private val fieldRegex = """"(login|password|email|phone|token)"\s*:\s*"[^"]+"""".toRegex() 15 | 16 | private fun clean(message: String): String { 17 | return message.replace(fieldRegex) { 18 | "\"${it.groupValues[1]}\":\"[OpenCord censored]\"" 19 | } 20 | } 21 | 22 | override fun error(tag: String, message: String?, throwable: Throwable?) { 23 | if (message != null || throwable != null) 24 | Log.e(tag, message, throwable) 25 | } 26 | 27 | override fun warn(tag: String, message: String, throwable: Throwable?) { 28 | Log.w(tag, message, throwable) 29 | } 30 | 31 | override fun info(tag: String, message: String) { 32 | Log.i(tag, message) 33 | } 34 | 35 | override fun debug(tag: String, message: String) { 36 | if (!BuildConfig.DEBUG) return 37 | 38 | for (part in clean(message).chunked(4000)) { 39 | Log.d(tag, part) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/util/NonceGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.util 2 | 3 | object NonceGenerator { 4 | private const val DISCORD_EPOCH = 1420070400000 5 | 6 | fun nowSnowflake(): String { 7 | val timestamp = System.currentTimeMillis() - DISCORD_EPOCH 8 | val snowflake = timestamp.toULong() shl 22 9 | return snowflake.toString() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/util/Quad.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.util 2 | 3 | data class Quad( 4 | val first: A, 5 | val second: B, 6 | val third: C, 7 | val fourth: D, 8 | ) { 9 | override fun toString() = "($first, $second, $third, $fourth)" 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/util/StdUtil.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.util 2 | 3 | inline fun Pair.map( 4 | first: (A) -> C, 5 | second: (B) -> D 6 | ): Pair { 7 | return first(this.first) to second(this.second) 8 | } 9 | 10 | inline fun MutableList.removeFirst(predicate: (T) -> Boolean): T? { 11 | return indexOfFirst(predicate) 12 | .takeIf { it >= 0 } 13 | ?.let { removeAt(it) } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/util/TextParser.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.util 2 | 3 | import com.xinto.simpleast.Node 4 | import com.xinto.simpleast.Parser 5 | 6 | typealias SimpleAstParser = Parser, Any?> 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/xinto/opencord/util/Throttling.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.opencord.util 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.Job 5 | import kotlinx.coroutines.delay 6 | import kotlinx.coroutines.launch 7 | 8 | /** 9 | * Constructs a function that executes [destinationFunction] only once per [skipMs]. 10 | */ 11 | inline fun throttle( 12 | skipMs: Long, 13 | coroutineScope: CoroutineScope, 14 | crossinline destinationFunction: suspend () -> Unit 15 | ): () -> Unit { 16 | var throttleJob: Job? = null 17 | return { 18 | if (throttleJob?.isCompleted != false) { 19 | throttleJob = coroutineScope.launch { 20 | destinationFunction() 21 | delay(skipMs) 22 | } 23 | } 24 | } 25 | } 26 | 27 | inline fun throttle( 28 | skipMs: Long, 29 | coroutineScope: CoroutineScope, 30 | crossinline destinationFunction: suspend (T) -> Unit 31 | ): (T) -> Unit { 32 | var throttleJob: Job? = null 33 | return { arg1 -> 34 | if (throttleJob?.isCompleted != false) { 35 | throttleJob = coroutineScope.launch { 36 | destinationFunction(arg1) 37 | delay(skipMs) 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_reaction.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_at_sign.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_cancel.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_channel_announcements.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_error.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file_copy.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_filter.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_guild_badge_premium_tier_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_guild_badge_premium_tier_1_banner.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_guild_badge_premium_tier_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_guild_badge_premium_tier_2_banner.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_guild_badge_premium_tier_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 13 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_guild_badge_premium_tier_3_banner.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 13 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_keyboard_arrow_down.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_link.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mark_unread.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_people.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 15 | 19 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pin.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_reply.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_status_dnd.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_status_idle.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_status_invisible.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_status_online.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_status_streaming.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tag.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_volume_up.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/font/inter_black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/app/src/main/res/font/inter_black.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/inter_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/app/src/main/res/font/inter_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/inter_extrabold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/app/src/main/res/font/inter_extrabold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/inter_extralight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/app/src/main/res/font/inter_extralight.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/inter_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/app/src/main/res/font/inter_light.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/inter_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/app/src/main/res/font/inter_medium.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/inter_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/app/src/main/res/font/inter_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/inter_semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/app/src/main/res/font/inter_semibold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/inter_thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/app/src/main/res/font/inter_thin.ttf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/raw/keep.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #4E5D94 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) apply false 3 | alias(libs.plugins.android.library) apply false 4 | alias(libs.plugins.kotlin.android) apply false 5 | alias(libs.plugins.kotlin.serialization) apply false 6 | alias(libs.plugins.kotlin.parcelize) apply false 7 | alias(libs.plugins.ksp) apply false 8 | } 9 | 10 | allprojects { 11 | repositories { 12 | google() 13 | mavenCentral() 14 | maven("https://jitpack.io") 15 | } 16 | } 17 | 18 | task("clean") { 19 | delete(rootProject.buildDir) 20 | delete(rootDir.resolve("buildSrc/build")) 21 | } 22 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | project_id_env: CROWDIN_PROJECT_ID 2 | api_token_env: CROWDIN_PERSONAL_TOKEN 3 | 4 | preserve_hierarchy: true 5 | files: 6 | - source: app/src/main/res/values/strings.xml 7 | translation: app/src/main/res/values-%android_code%/strings.xml 8 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Specifies the JVM arguments used for the daemon process. 2 | # The setting is particularly useful for tweaking memory settings. 3 | org.gradle.jvmargs=-Xmx3072m -Dfile.encoding=UTF-8 4 | 5 | # When configured, Gradle will run in incubating parallel mode. 6 | # This option should only be used with decoupled projects. More details, visit 7 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 8 | org.gradle.parallel=true 9 | 10 | # AndroidX package structure to make it clearer which packages are bundled with the 11 | # Android operating system, and which are packaged with your app"s APK 12 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 13 | android.useAndroidX=true 14 | 15 | # Automatically convert third-party libraries to use AndroidX 16 | android.enableJetifier=false 17 | 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | 21 | # Enable R8 full mode. 22 | android.enableR8.fullMode=true 23 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Apr 11 10:48:47 GET 2023 2 | networkTimeout=10000 3 | distributionBase=GRADLE_USER_HOME 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip 5 | distributionPath=wrapper/dists 6 | zipStorePath=wrapper/dists 7 | zipStoreBase=GRADLE_USER_HOME 8 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | } 7 | } 8 | 9 | rootProject.name = "OpenCord" 10 | 11 | include(":app") 12 | include(":simpleast-compose") -------------------------------------------------------------------------------- /simpleast-compose/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /simpleast-compose/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | alias(libs.plugins.android.library) 5 | alias(libs.plugins.kotlin.android) 6 | } 7 | 8 | android { 9 | namespace = "com.xinto.simpleast" 10 | compileSdk = 32 11 | 12 | defaultConfig { 13 | minSdk = 21 14 | 15 | consumerProguardFiles("consumer-rules.pro") 16 | } 17 | 18 | buildTypes { 19 | release { 20 | isMinifyEnabled = false 21 | proguardFiles( 22 | getDefaultProguardFile("proguard-android-optimize.txt"), 23 | "proguard-rules.pro", 24 | ) 25 | } 26 | } 27 | 28 | buildFeatures { 29 | buildConfig = false 30 | resValues = false 31 | } 32 | 33 | compileOptions { 34 | sourceCompatibility = JavaVersion.VERSION_11 35 | targetCompatibility = JavaVersion.VERSION_11 36 | } 37 | 38 | kotlinOptions { 39 | jvmTarget = "11" 40 | freeCompilerArgs += listOf( 41 | "-Xcontext-receivers" 42 | ) 43 | } 44 | } 45 | 46 | kotlin { 47 | explicitApi() 48 | } 49 | 50 | dependencies { 51 | implementation(libs.androidx.compose.foundation) 52 | } 53 | -------------------------------------------------------------------------------- /simpleast-compose/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MateriiApps/OpenCord/b06fcaf6aec056030cda4930cd671f38835f86e3/simpleast-compose/consumer-rules.pro -------------------------------------------------------------------------------- /simpleast-compose/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /simpleast-compose/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /simpleast-compose/src/main/java/com/xinto/simpleast/DSL.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.simpleast 2 | 3 | import java.util.regex.Matcher 4 | import java.util.regex.Pattern 5 | 6 | public inline fun , S> createRule( 7 | pattern: Pattern, 8 | crossinline block: (Matcher, Parser, state: S) -> ParseSpec 9 | ): Rule { 10 | return object : Rule(pattern) { 11 | override fun parse( 12 | matcher: Matcher, 13 | parser: Parser, 14 | state: S 15 | ): ParseSpec { 16 | return block(matcher, parser, state) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /simpleast-compose/src/main/java/com/xinto/simpleast/Node.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.simpleast 2 | 3 | import androidx.compose.ui.text.AnnotatedString 4 | 5 | public open class Node( 6 | private var children: MutableCollection>? = null, 7 | ) { 8 | public val nodeChildren: Collection>? 9 | get() = children 10 | 11 | public val hasChildren: Boolean 12 | get() = children?.isNotEmpty() == true 13 | 14 | public fun addChild(node: Node) { 15 | if (children == null) { 16 | children = ArrayList() 17 | } 18 | 19 | children!!.add(node) 20 | } 21 | 22 | //@formatter:off 23 | context(AnnotatedString.Builder) 24 | public open fun render(renderContext: RC): Unit = Unit 25 | //@formatter:on 26 | 27 | public open class Parent( 28 | vararg children: Node?, 29 | ) : Node(children.mapNotNull { it }.toMutableList()) { 30 | //@formatter:off 31 | context(AnnotatedString.Builder) 32 | override fun render(renderContext: RC) { //@formatter:on 33 | nodeChildren?.forEach { 34 | it.render(renderContext) 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /simpleast-compose/src/main/java/com/xinto/simpleast/ParseException.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.simpleast 2 | 3 | public class ParseException( 4 | message: String, 5 | source: CharSequence?, 6 | cause: Throwable? = null, 7 | ) : RuntimeException("Error while parsing: $message \n Source: $source", cause) 8 | -------------------------------------------------------------------------------- /simpleast-compose/src/main/java/com/xinto/simpleast/ParseSpec.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.simpleast 2 | 3 | public class ParseSpec private constructor( 4 | public val root: Node, 5 | public val isTerminal: Boolean, 6 | public val state: S, 7 | public var startIndex: Int, 8 | public var endIndex: Int, 9 | ) { 10 | public fun applyOffset(offset: Int) { 11 | startIndex += offset 12 | endIndex += offset 13 | } 14 | 15 | public companion object { 16 | public fun createNonTerminal( 17 | node: Node, 18 | state: S, 19 | startIndex: Int, 20 | endIndex: Int, 21 | ): ParseSpec { 22 | return ParseSpec( 23 | root = node, 24 | isTerminal = false, 25 | state = state, 26 | startIndex = startIndex, 27 | endIndex = endIndex, 28 | ) 29 | } 30 | 31 | public fun createTerminal( 32 | node: Node, 33 | state: S, 34 | ): ParseSpec { 35 | return ParseSpec( 36 | root = node, 37 | isTerminal = true, 38 | state = state, 39 | startIndex = 0, 40 | endIndex = 0, 41 | ) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /simpleast-compose/src/main/java/com/xinto/simpleast/Renderer.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.simpleast 2 | 3 | import androidx.compose.ui.text.AnnotatedString 4 | 5 | public fun Parser, S>.render( 6 | source: String, 7 | initialState: S, 8 | renderContext: RC, 9 | ): AnnotatedString.Builder { 10 | return render( 11 | builder = AnnotatedString.Builder(), 12 | nodes = parse(source, initialState), 13 | renderContext = renderContext, 14 | ) 15 | } 16 | 17 | public fun render( 18 | builder: AnnotatedString.Builder = AnnotatedString.Builder(), 19 | nodes: Collection>, 20 | renderContext: RC, 21 | ): AnnotatedString.Builder { 22 | return builder.apply { 23 | for (node in nodes) { 24 | node.render(renderContext) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /simpleast-compose/src/main/java/com/xinto/simpleast/Rule.kt: -------------------------------------------------------------------------------- 1 | package com.xinto.simpleast 2 | 3 | import java.util.regex.Matcher 4 | import java.util.regex.Pattern 5 | 6 | public abstract class Rule, S>( 7 | private val matcher: Matcher 8 | ) { 9 | public constructor(pattern: Pattern) : this(pattern.matcher("")) 10 | 11 | public open fun match( 12 | inspectionSource: CharSequence, 13 | lastCapture: String?, 14 | state: S, 15 | ): Matcher? { 16 | matcher.reset(inspectionSource) 17 | return if (matcher.find()) matcher else null 18 | } 19 | 20 | public abstract fun parse( 21 | matcher: Matcher, 22 | parser: Parser, state: S, 23 | ): ParseSpec 24 | 25 | public abstract class BlockRule, S>( 26 | pattern: Pattern, 27 | ) : Rule(pattern) { 28 | 29 | override fun match( 30 | inspectionSource: CharSequence, 31 | lastCapture: String?, 32 | state: S, 33 | ): Matcher? { 34 | if (lastCapture?.endsWith('\n') != false) { 35 | return super.match(inspectionSource, lastCapture, state) 36 | } 37 | return null 38 | } 39 | } 40 | } 41 | --------------------------------------------------------------------------------