From 3503ecffab25015c31b8b47463c0c2ea278fea0f Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Thu, 11 Jul 2024 02:12:32 +0300 Subject: [PATCH] Upstream changes (#23) --- .github/workflows/android_dev.yml | 48 + .github/workflows/android_master.yml | 47 + .gitignore | 5 +- README.md | 81 +- app/.gitignore | 4 +- app/build.gradle.kts | 288 ++-- app/keystore/keystore.jks | Bin 0 -> 2409 bytes app/proguard-rules.pro | 2 +- .../2.json | 52 + .../3.json | 424 +++++ .../com/meloda/fast/tests/LoginSignInTest.kt | 46 + app/src/debug/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3275 bytes app/src/debug/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1980 bytes .../debug/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4585 bytes .../debug/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7262 bytes .../debug/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10648 bytes .../res/values/ic_launcher_background.xml} | 4 +- app/src/debug/res/values/strings.xml | 4 + app/src/dev/res/mipmap-hdpi/ic_launcher.png | Bin 3313 -> 0 bytes app/src/dev/res/mipmap-mdpi/ic_launcher.png | Bin 1983 -> 0 bytes app/src/dev/res/mipmap-xhdpi/ic_launcher.png | Bin 4669 -> 0 bytes app/src/dev/res/mipmap-xxhdpi/ic_launcher.png | Bin 7322 -> 0 bytes .../dev/res/mipmap-xxxhdpi/ic_launcher.png | Bin 10692 -> 0 bytes app/src/main/AndroidManifest.xml | 64 +- .../com/meloda/app/fast/MainActivity.kt | 254 +++ .../kotlin/com/meloda/app/fast/MainGraph.kt | 196 +++ .../com/meloda/app/fast/MainViewModel.kt | 147 ++ .../kotlin/com/meloda/app/fast/RootGraph.kt | 92 + .../com/meloda/app/fast/common/AppGlobal.kt | 35 + .../app/fast/common/di/ApplicationModule.kt | 47 + .../meloda/app/fast/model/LongPollState.kt | 7 + .../meloda/app/fast/model/MainScreenState.kt | 30 + .../meloda/app/fast/model/ServicesState.kt | 7 + .../fast/receiver/DownloadManagerReceiver.kt | 4 +- .../meloda/app/fast/service/OnlineService.kt | 129 ++ .../service/longpolling/LongPollingService.kt | 271 +++ .../service/longpolling/di/LongPollModule.kt | 13 + .../{ => app}/fast/util/NotificationsUtils.kt | 7 +- .../kotlin/com/meloda/fast/api/ApiEvent.kt | 26 - .../com/meloda/fast/api/ApiExtensions.kt | 7 - .../kotlin/com/meloda/fast/api/VkUtils.kt | 1285 -------------- .../com/meloda/fast/api/base/ApiError.kt | 20 - .../com/meloda/fast/api/base/ApiResponse.kt | 8 - .../AttachmentClassNameIsEmptyException.kt | 9 - .../meloda/fast/api/longpoll/LongPollEvent.kt | 34 - .../api/longpoll/LongPollUpdatesParser.kt | 300 ---- .../com/meloda/fast/api/model/ActionState.kt | 17 - .../fast/api/model/ConversationPeerType.kt | 25 - .../com/meloda/fast/api/model/VkChatMember.kt | 14 - .../com/meloda/fast/api/model/VkGroup.kt | 21 - .../com/meloda/fast/api/model/VkMessage.kt | 132 -- .../com/meloda/fast/api/model/VkUser.kt | 26 - .../api/model/attachments/VkAttachment.kt | 12 - .../fast/api/model/attachments/VkAudio.kt | 28 - .../fast/api/model/attachments/VkCall.kt | 19 - .../fast/api/model/attachments/VkCurator.kt | 13 - .../fast/api/model/attachments/VkEvent.kt | 8 - .../fast/api/model/attachments/VkFile.kt | 30 - .../fast/api/model/attachments/VkGift.kt | 16 - .../fast/api/model/attachments/VkGraffiti.kt | 18 - .../fast/api/model/attachments/VkGroupCall.kt | 13 - .../fast/api/model/attachments/VkLink.kt | 18 - .../fast/api/model/attachments/VkMiniApp.kt | 13 - .../fast/api/model/attachments/VkPoll.kt | 13 - .../fast/api/model/attachments/VkSticker.kt | 26 - .../fast/api/model/attachments/VkStory.kt | 21 - .../api/model/attachments/VkVoiceMessage.kt | 22 - .../fast/api/model/attachments/VkWall.kt | 25 - .../fast/api/model/attachments/VkWallReply.kt | 13 - .../fast/api/model/attachments/VkWidget.kt | 13 - .../meloda/fast/api/model/base/BaseVkChat.kt | 38 - .../fast/api/model/base/BaseVkChatMember.kt | 26 - .../meloda/fast/api/model/base/BaseVkGroup.kt | 31 - .../fast/api/model/base/BaseVkLongPoll.kt | 12 - .../fast/api/model/base/BaseVkMessage.kt | 76 - .../meloda/fast/api/model/base/BaseVkUser.kt | 47 - .../base/attachments/BaseVkAttachmentItem.kt | 77 - .../api/model/base/attachments/BaseVkAudio.kt | 60 - .../api/model/base/attachments/BaseVkCall.kt | 26 - .../model/base/attachments/BaseVkCurator.kt | 27 - .../api/model/base/attachments/BaseVkEvent.kt | 20 - .../api/model/base/attachments/BaseVkFile.kt | 63 - .../api/model/base/attachments/BaseVkGift.kt | 22 - .../model/base/attachments/BaseVkGraffiti.kt | 26 - .../model/base/attachments/BaseVkGroupCall.kt | 22 - .../api/model/base/attachments/BaseVkLink.kt | 25 - .../model/base/attachments/BaseVkMiniApp.kt | 69 - .../api/model/base/attachments/BaseVkPhoto.kt | 43 - .../api/model/base/attachments/BaseVkPoll.kt | 63 - .../model/base/attachments/BaseVkSticker.kt | 37 - .../api/model/base/attachments/BaseVkStory.kt | 52 - .../api/model/base/attachments/BaseVkVideo.kt | 140 -- .../base/attachments/BaseVkVoiceMessage.kt | 32 - .../api/model/base/attachments/BaseVkWall.kt | 78 - .../model/base/attachments/BaseVkWidget.kt | 11 - .../fast/api/model/data/BaseVkConversation.kt | 147 -- .../api/model/domain/VkConversationDomain.kt | 245 --- .../model/presentation/VkConversationUi.kt | 33 - .../com/meloda/fast/api/network/ApiErrors.kt | 110 -- .../fast/api/network/AuthInterceptor.kt | 32 - .../fast/api/network/ResultCallFactory.kt | 155 -- .../com/meloda/fast/api/network/VkUrls.kt | 9 - .../fast/api/network/account/AccountUrls.kt | 10 - .../fast/api/network/audio/AudiosRequests.kt | 2 - .../fast/api/network/audio/AudiosResponses.kt | 20 - .../fast/api/network/audio/AudiosUrls.kt | 11 - .../fast/api/network/auth/AuthResponse.kt | 26 - .../meloda/fast/api/network/auth/AuthUrls.kt | 10 - .../conversations/ConversationsResponse.kt | 26 - .../conversations/ConversationsUrls.kt | 13 - .../fast/api/network/files/FileRequests.kt | 2 - .../fast/api/network/files/FilesResponses.kt | 25 - .../fast/api/network/files/FilesUrls.kt | 11 - .../api/network/messages/MessagesResponse.kt | 31 - .../fast/api/network/messages/MessagesUrls.kt | 22 - .../fast/api/network/ota/OtaResponses.kt | 8 - .../meloda/fast/api/network/ota/OtaUrls.kt | 7 - .../fast/api/network/photos/PhotoUrls.kt | 11 - .../fast/api/network/photos/PhotosRequests.kt | 16 - .../api/network/photos/PhotosResponses.kt | 18 - .../fast/api/network/users/UsersResponse.kt | 2 - .../fast/api/network/users/UsersUrls.kt | 9 - .../fast/api/network/videos/VideosRequests.kt | 2 - .../api/network/videos/VideosResponses.kt | 35 - .../fast/api/network/videos/VideosUrls.kt | 9 - .../com/meloda/fast/base/BaseActivity.kt | 11 - .../com/meloda/fast/base/BaseFragment.kt | 11 - .../com/meloda/fast/base/ResourceProvider.kt | 26 - .../fast/base/adapter/AsyncDiffItemAdapter.kt | 52 - .../meloda/fast/base/adapter/BaseAdapter.kt | 281 --- .../com/meloda/fast/base/adapter/Holders.kt | 14 - .../com/meloda/fast/base/adapter/Listeners.kt | 9 - .../com/meloda/fast/base/screen/AppScreen.kt | 23 - .../fast/base/viewmodel/BaseViewModel.kt | 97 -- .../base/viewmodel/BaseViewModelFragment.kt | 34 - .../base/viewmodel/DeprecatedBaseViewModel.kt | 139 -- .../fast/base/viewmodel/ErrorHandler.kt | 9 - .../com/meloda/fast/base/viewmodel/Events.kt | 30 - .../fast/base/viewmodel/ViewModelUtils.kt | 71 - .../com/meloda/fast/common/AppConstants.kt | 7 - .../com/meloda/fast/common/AppGlobal.kt | 83 - .../kotlin/com/meloda/fast/common/Screens.kt | 65 - .../com/meloda/fast/common/UpdateManager.kt | 116 -- .../fast/common/di/ApplicationModule.kt | 40 - .../kotlin/com/meloda/fast/compose/Dialogs.kt | 163 -- .../meloda/fast/data/account/AccountApi.kt | 18 - .../meloda/fast/data/account/AccountsDao.kt | 21 - .../fast/data/account/AccountsRepository.kt | 17 - .../com/meloda/fast/data/audios/AudiosApi.kt | 28 - .../fast/data/audios/AudiosRepository.kt | 21 - .../com/meloda/fast/data/auth/AuthApi.kt | 19 - .../meloda/fast/data/auth/AuthRepository.kt | 14 - .../data/conversations/ConversationsApi.kt | 33 - .../data/conversations/ConversationsDao.kt | 18 - .../conversations/ConversationsRepository.kt | 24 - .../com/meloda/fast/data/files/FilesApi.kt | 33 - .../meloda/fast/data/files/FilesRepository.kt | 30 - .../com/meloda/fast/data/groups/GroupsDao.kt | 23 - .../fast/data/groups/GroupsRepository.kt | 6 - .../meloda/fast/data/longpoll/LongPollApi.kt | 17 - .../meloda/fast/data/messages/MessagesApi.kt | 70 - .../meloda/fast/data/messages/MessagesDao.kt | 26 - .../fast/data/messages/MessagesRepository.kt | 104 -- .../kotlin/com/meloda/fast/data/ota/OtaApi.kt | 26 - .../com/meloda/fast/data/photos/PhotosApi.kt | 33 - .../fast/data/photos/PhotosRepository.kt | 18 - .../com/meloda/fast/data/users/UsersApi.kt | 19 - .../com/meloda/fast/data/users/UsersDao.kt | 23 - .../meloda/fast/data/users/UsersRepository.kt | 17 - .../com/meloda/fast/data/videos/VideosApi.kt | 26 - .../fast/data/videos/VideosRepository.kt | 13 - .../meloda/fast/database/AccountsDatabase.kt | 15 - .../com/meloda/fast/database/CacheDatabase.kt | 33 - .../com/meloda/fast/database/Converters.kt | 160 -- .../kotlin/com/meloda/fast/di/ApiModule.kt | 35 - .../kotlin/com/meloda/fast/di/DataModule.kt | 26 - .../com/meloda/fast/di/DatabaseModule.kt | 28 - .../com/meloda/fast/di/NavigationModule.kt | 20 - .../com/meloda/fast/di/NetworkModule.kt | 53 - .../kotlin/com/meloda/fast/di/OtaModule.kt | 13 - .../kotlin/com/meloda/fast/ext/ActivityExt.kt | 14 - .../com/meloda/fast/ext/AndroidVersionsExt.kt | 54 - .../kotlin/com/meloda/fast/ext/BooleanExt.kt | 5 - .../kotlin/com/meloda/fast/ext/BundleExt.kt | 36 - .../kotlin/com/meloda/fast/ext/ComposeExt.kt | 146 -- .../kotlin/com/meloda/fast/ext/ContextExt.kt | 90 - .../main/kotlin/com/meloda/fast/ext/Ext.kt | 148 -- .../kotlin/com/meloda/fast/ext/FragmentExt.kt | 47 - .../kotlin/com/meloda/fast/ext/GlideExt.kt | 189 -- .../kotlin/com/meloda/fast/ext/NumbersExt.kt | 1 - .../kotlin/com/meloda/fast/ext/StringExt.kt | 30 - .../kotlin/com/meloda/fast/ext/ViewExt.kt | 196 --- .../com/meloda/fast/model/SelectableItem.kt | 15 - .../com/meloda/fast/model/UpdateItem.kt | 53 - .../meloda/fast/model/base/AdapterDiffItem.kt | 14 - .../meloda/fast/model/base/DisplayableItem.kt | 3 - .../com/meloda/fast/model/base/UiImage.kt | 94 - .../receiver/StopLongPollServiceReceiver.kt | 36 - .../fast/screens/captcha/CaptchaScreens.kt | 11 - .../fast/screens/captcha/di/CaptchaDI.kt | 36 - .../captcha/model/CaptchaValidationResult.kt | 8 - .../presentation/CaptchaCoordinator.kt | 21 - .../captcha/presentation/CaptchaFragment.kt | 234 --- .../captcha/screen/CaptchaArguments.kt | 3 - .../screens/captcha/screen/CaptchaResult.kt | 6 - .../screens/captcha/screen/CaptchaScreen.kt | 21 - .../fast/screens/chatinfo/ChatInfoFragment.kt | 299 ---- .../chatinfo/ChatInfoMembersAdapter.kt | 111 -- .../chatinfo/ChatInfoMembersFragment.kt | 83 - .../screens/chatinfo/ChatInfoPagerAdapter.kt | 18 - .../screens/chatinfo/ChatInfoViewModel.kt | 58 - .../fast/screens/chatinfo/di/ChatInfoDI.kt | 9 - .../conversations/ConversationCompose.kt | 256 --- .../conversations/ConversationsFragment.kt | 342 ---- .../ConversationsResourceProvider.kt | 26 - .../conversations/ConversationsViewModel.kt | 573 ------ .../adapter/ConversationsDelegates.kt | 172 -- .../conversations/di/ConversationsModule.kt | 9 - .../fast/screens/login/LoginFragment.kt | 419 ----- .../meloda/fast/screens/login/LoginScreens.kt | 10 - .../fast/screens/login/LoginViewModel.kt | 370 ---- .../fast/screens/login/di/LoginModule.kt | 11 - .../fast/screens/login/model/LoginResult.kt | 6 - .../login/model/LoginValidationResult.kt | 12 - .../fast/screens/login/screen/LoginScreen.kt | 16 - .../meloda/fast/screens/main/MainFragment.kt | 59 - .../meloda/fast/screens/main/MainViewModel.kt | 60 - .../screens/main/activity/LongPollState.kt | 7 - .../screens/main/activity/LongPollUtils.kt | 129 -- .../screens/main/activity/MainActivity.kt | 316 ---- .../screens/main/activity/ServicesState.kt | 7 - .../meloda/fast/screens/main/di/MainModule.kt | 9 - .../screens/messages/AttachmentInflater.kt | 628 ------- .../screens/messages/AttachmentsAdapter.kt | 237 --- .../messages/ForwardedMessagesFragment.kt | 118 -- .../messages/MessagesHistoryAdapter.kt | 377 ---- .../messages/MessagesHistoryFragment.kt | 1531 ----------------- .../messages/MessagesHistoryViewModel.kt | 598 ------- .../screens/messages/MessagesPreparator.kt | 303 ---- .../messages/di/MessagesHistoryModule.kt | 9 - .../fast/screens/photos/PhotoViewFragment.kt | 45 - .../fast/screens/photos/PhotoViewViewModel.kt | 17 - .../fast/screens/photos/di/PhotoViewDI.kt | 9 - .../fast/screens/settings/SettingsFragment.kt | 338 ---- .../screens/settings/SettingsViewModel.kt | 526 ------ .../screens/settings/di/SettingsModule.kt | 9 - .../settings/items/ListSettingsItem.kt | 122 -- .../settings/items/TextFieldSettingsItem.kt | 187 -- .../items/TitleSummarySettingsItem.kt | 77 - .../fast/screens/testing/TestActivity.kt | 61 - .../meloda/fast/screens/twofa/TwoFaScreens.kt | 12 - .../meloda/fast/screens/twofa/di/TwoFaDI.kt | 35 - .../screens/twofa/model/TwoFaArguments.kt | 12 - .../fast/screens/twofa/model/TwoFaResult.kt | 6 - .../screens/twofa/model/TwoFaScreenState.kt | 24 - .../twofa/model/TwoFaValidationResult.kt | 8 - .../twofa/presentation/TwoFaViewModel.kt | 172 -- .../screens/twofa/screen/TwoFaCoordinator.kt | 21 - .../fast/screens/twofa/screen/TwoFaScreen.kt | 21 - .../twofa/validation/TwoFaValidator.kt | 14 - .../fast/screens/updates/UpdatesFragment.kt | 391 ----- .../fast/screens/updates/UpdatesViewModel.kt | 375 ---- .../fast/screens/updates/di/UpdatesDI.kt | 9 - .../fast/screens/updates/model/UpdateState.kt | 15 - .../updates/model/UpdatesScreenState.kt | 22 - .../screens/userbanned/UserBannedFragment.kt | 55 - .../fast/service/LongPollQSTileService.kt | 43 - .../meloda/fast/service/LongPollService.kt | 281 --- .../fast/service/MyCustomControlService.kt | 59 - .../com/meloda/fast/service/OnlineService.kt | 106 -- .../kotlin/com/meloda/fast/ui/AppTheme.kt | 93 - .../com/meloda/fast/ui/BlueColorScheme.kt | 74 - .../com/meloda/fast/ui/GreenColorScheme.kt | 74 - .../com/meloda/fast/ui/RedColorScheme.kt | 74 - .../kotlin/com/meloda/fast/ui/colors/Blue.kt | 71 - .../kotlin/com/meloda/fast/ui/colors/Green.kt | 71 - .../kotlin/com/meloda/fast/ui/colors/Red.kt | 70 - .../com/meloda/fast/ui/widgets/AsyncImage.kt | 48 - .../kotlin/com/meloda/fast/util/ColorUtils.kt | 18 - .../kotlin/com/meloda/fast/util/TimeUtils.kt | 75 - .../meloda/fast/view/BoundedLinearLayout.kt | 62 - .../com/meloda/fast/view/CircleImageView.kt | 61 - .../com/meloda/fast/view/DialogToolbar.kt | 119 -- .../meloda/fast/view/SpaceItemDecoration.kt | 26 - .../main/res/anim/activity_close_enter.xml | 17 - app/src/main/res/anim/activity_close_exit.xml | 28 - app/src/main/res/anim/activity_open_enter.xml | 27 - app/src/main/res/anim/activity_open_exit.xml | 27 - .../main/res/anim/fast_out_extra_slow_in.xml | 3 - .../ic_notification_new_message.png | Bin 373 -> 0 bytes .../ic_notification_new_message.png | Bin 243 -> 0 bytes .../ic_notification_new_message.png | Bin 484 -> 0 bytes .../ic_notification_new_message.png | Bin 711 -> 0 bytes app/src/main/res/drawable/ic_back.xml | 7 - .../ic_baseline_account_circle_24.xml | 10 - .../ic_chat_attachment_panel_background.xml | 11 - .../main/res/drawable/ic_close_in_circle.xml | 7 - .../ic_image_button_circle_background.xml | 9 - app/src/main/res/drawable/ic_key.xml | 10 - ..._message_attachment_story_image_dimmer.xml | 10 - .../res/drawable/ic_message_in_background.xml | 13 - .../ic_message_in_background_middle.xml | 9 - .../drawable/ic_message_out_background.xml | 17 - .../ic_message_out_background_middle.xml | 13 - ...c_message_out_background_middle_stroke.xml | 13 - .../ic_message_out_background_stroke.xml | 13 - .../drawable/ic_message_panel_background.xml | 9 - .../drawable/ic_message_panel_gradient.xml | 13 - .../main/res/drawable/ic_message_unread.xml | 10 - ...es_history_toolbar_gradient_background.xml | 13 - .../drawable/ic_notification_new_message.xml | 15 - app/src/main/res/drawable/ic_online_pc.xml | 15 - .../main/res/drawable/ic_people_outline.xml | 10 - app/src/main/res/drawable/ic_phantom.xml | 10 - .../ic_play_button_circle_background.xml | 7 - .../res/drawable/ic_round_access_time_24.xml | 9 - .../main/res/drawable/ic_round_close_20.xml | 9 - .../drawable/ic_round_emoji_emotions_24.xml | 9 - .../drawable/ic_round_error_outline_24.xml | 9 - .../main/res/drawable/ic_round_group_24.xml | 9 - .../ic_round_keyboard_arrow_down_24.xml | 9 - .../main/res/drawable/ic_round_link_24.xml | 10 - .../main/res/drawable/ic_round_mail_24.xml | 9 - app/src/main/res/drawable/ic_round_mic_24.xml | 9 - .../res/drawable/ic_round_play_arrow_24.xml | 9 - .../main/res/drawable/ic_round_send_24.xml | 11 - .../res/drawable/ic_round_settings_24.xml | 9 - .../drawable/ic_round_settings_primary.xml | 11 - .../main/res/drawable/ic_round_star_24.xml | 9 - app/src/main/res/drawable/ic_search.xml | 9 - app/src/main/res/drawable/ic_security.xml | 10 - app/src/main/res/drawable/ic_star_border.xml | 10 - ...ad_indicator_on_attachments_background.xml | 9 - app/src/main/res/font/tt_commons_bold.ttf | Bin 116140 -> 0 bytes app/src/main/res/font/tt_commons_medium.ttf | Bin 194384 -> 0 bytes app/src/main/res/font/tt_commons_regular.ttf | Bin 194848 -> 0 bytes app/src/main/res/layout/activity_main.xml | 5 - app/src/main/res/layout/dialog_captcha.xml | 81 - app/src/main/res/layout/dialog_fast_login.xml | 21 - .../main/res/layout/dialog_message_delete.xml | 17 - app/src/main/res/layout/dialog_validation.xml | 71 - app/src/main/res/layout/drawer_header.xml | 29 - .../main/res/layout/fragment_chat_info.xml | 52 - .../res/layout/fragment_chat_info_members.xml | 25 - .../res/layout/fragment_conversations.xml | 63 - .../layout/fragment_forwarded_messages.xml | 25 - .../res/layout/fragment_messages_history.xml | 297 ---- app/src/main/res/layout/fragment_settings.xml | 42 - app/src/main/res/layout/fragment_updates.xml | 105 -- .../main/res/layout/fragment_user_banned.xml | 71 - app/src/main/res/layout/item_chat_member.xml | 78 - app/src/main/res/layout/item_conversation.xml | 256 --- .../layout/item_message_attachment_audio.xml | 53 - .../layout/item_message_attachment_call.xml | 54 - .../layout/item_message_attachment_file.xml | 53 - .../item_message_attachment_forwards.xml | 14 - .../layout/item_message_attachment_geo.xml | 54 - .../layout/item_message_attachment_gift.xml | 11 - .../item_message_attachment_graffiti.xml | 11 - .../layout/item_message_attachment_link.xml | 60 - .../layout/item_message_attachment_photo.xml | 32 - .../layout/item_message_attachment_reply.xml | 49 - .../item_message_attachment_sticker.xml | 11 - .../layout/item_message_attachment_story.xml | 52 - .../layout/item_message_attachment_video.xml | 46 - .../layout/item_message_attachment_voice.xml | 54 - .../item_message_attachment_wall_post.xml | 61 - app/src/main/res/layout/item_message_in.xml | 140 -- app/src/main/res/layout/item_message_out.xml | 114 -- .../main/res/layout/item_message_service.xml | 31 - .../res/layout/item_settings_checkbox.xml | 50 - .../res/layout/item_settings_edit_text.xml | 40 - .../layout/item_settings_edit_text_alert.xml | 24 - .../main/res/layout/item_settings_list.xml | 40 - .../main/res/layout/item_settings_switch.xml | 50 - .../main/res/layout/item_settings_title.xml | 19 - .../layout/item_settings_title_summary.xml | 40 - .../layout/item_uploaded_attachment_audio.xml | 55 - .../layout/item_uploaded_attachment_file.xml | 73 - .../layout/item_uploaded_attachment_photo.xml | 36 - .../layout/item_uploaded_attachment_video.xml | 73 - .../res/layout/toolbar_menu_item_avatar.xml | 21 - .../main/res/layout/view_dialog_toolbar.xml | 74 - .../main/res/menu/activity_main_bottom.xml | 19 - .../main/res/menu/activity_main_drawer.xml | 29 - .../main/res/menu/fragment_conversations.xml | 17 - .../res/menu/fragment_conversations_popup.xml | 12 - app/src/main/res/values-night/bools.xml | 7 - app/src/main/res/values-night/colors.xml | 5 - app/src/main/res/values-ru/strings.xml | 6 - app/src/main/res/values-v27/themes.xml | 12 - app/src/main/res/values-v31/monet_colors.xml | 51 - app/src/main/res/values/attrs.xml | 48 - app/src/main/res/values/bools.xml | 7 - app/src/main/res/values/colors.xml | 57 - app/src/main/res/values/dimens.xml | 7 - app/src/main/res/values/monet_colors.xml | 50 - app/src/main/res/values/styles.xml | 41 - app/src/main/res/values/themes.xml | 32 - app/src/main/res/xml/locales_config.xml | 1 + .../main/res/xml/network_security_config.xml | 16 + app/src/main/res/xml/preferences.xml | 79 - app/src/main/res/xml/shortcuts.xml | 21 - .../staging/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3284 bytes .../staging/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1977 bytes .../staging/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4572 bytes .../staging/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7245 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10585 bytes .../res/values/ic_launcher_background.xml | 0 app/src/staging/res/values/strings.xml | 4 + build.gradle.kts | 46 +- buildSrc/build.gradle.kts | 7 + buildSrc/settings.gradle.kts | 1 + buildSrc/src/main/kotlin/Configs.kt | 13 + core/common/.gitignore | 1 + core/common/build.gradle.kts | 57 + core/common/src/main/AndroidManifest.xml | 4 + .../meloda/app/fast/common/AppConstants.kt | 10 + .../meloda/app/fast/common/AuthInterceptor.kt | 31 + .../meloda/app/fast/common/CustomNavType.kt | 22 + .../com/meloda/app/fast/common/UiImage.kt | 29 + .../com/meloda/app/fast/common}/UiText.kt | 24 +- .../com/meloda/app/fast/common}/UserConfig.kt | 24 +- .../com/meloda/app/fast/common/VkConstants.kt | 33 +- .../meloda/app/fast/common/di/CommonModule.kt | 12 + .../app/fast/common/extensions/Extensions.kt | 151 ++ .../common/extensions/StringsExtensions.kt | 17 + .../navigation/NavigationExtensions.kt | 18 + .../app/fast/common}/util/AndroidUtils.kt | 84 +- .../meloda/app/fast/common/util/TimeUtils.kt | 83 + .../meloda/app/fast/common/util/VkUtils.kt | 721 ++++++++ core/common/src/main/res/values/strings.xml | 10 + core/data/.gitignore | 1 + core/data/build.gradle.kts | 42 + core/data/src/main/AndroidManifest.xml | 4 + .../app/fast/data/LongPollUpdatesParser.kt | 354 ++++ .../meloda/app/fast/data/LongPollUseCase.kt | 23 + .../app/fast/data/LongPollUseCaseImpl.kt | 49 + .../kotlin/com/meloda/app/fast/data/State.kt | 76 + .../com/meloda/app/fast/data/VkGroupsMap.kt | 47 + .../com/meloda/app/fast/data/VkMemoryCache.kt | 119 ++ .../com/meloda/app/fast/data/VkUsersMap.kt | 46 + .../data/api/account/AccountRepository.kt | 15 + .../data/api/account/AccountRepositoryImpl.kt | 19 + .../fast/data/api/account/AccountUseCase.kt | 16 + .../data/api/account/AccountUseCaseImpl.kt | 49 + .../fast/data/api/audios/AudiosRepository.kt | 26 + .../app/fast/data/api/auth/AuthRepository.kt | 18 + .../fast/data/api/auth/AuthRepositoryImpl.kt | 35 + .../conversations/ConversationsRepository.kt | 18 + .../ConversationsRepositoryImpl.kt | 109 ++ .../api/conversations/ConversationsUseCase.kt | 19 + .../fast/data/api/files/FilesRepository.kt | 31 + .../data/api/friends/FriendsRepository.kt | 26 + .../data/api/friends/FriendsRepositoryImpl.kt | 83 + .../fast/data/api/friends/FriendsUseCase.kt | 26 + .../data/api/longpoll/LongPollRepository.kt | 24 + .../api/longpoll/LongPollRepositoryImpl.kt | 57 + .../api/messages/MessagesLocalDataSource.kt | 16 + .../messages/MessagesLocalDataSourceImpl.kt | 24 + .../api/messages/MessagesNetworkDataSource.kt | 36 + .../messages/MessagesNetworkDataSourceImpl.kt | 164 ++ .../data/api/messages/MessagesRepository.kt | 75 + .../api/messages/MessagesRepositoryImpl.kt | 201 +++ .../fast/data/api/messages/MessagesUseCase.kt | 43 + .../fast/data/api/oauth/OAuthRepository.kt | 15 + .../data/api/oauth/OAuthRepositoryImpl.kt | 51 + .../fast/data/api/photos/PhotosRepository.kt | 19 + .../fast/data/api/users/UsersRepository.kt | 8 + .../data/api/users/UsersRepositoryImpl.kt | 16 + .../app/fast/data/api/users/UsersUseCase.kt | 23 + .../fast/data/api/users/UsersUseCaseImpl.kt | 67 + .../fast/data/api/videos/VideosRepository.kt | 14 + .../app/fast/data/db/AccountsRepository.kt | 10 + .../fast/data/db/AccountsRepositoryImpl.kt | 15 + .../com/meloda/app/fast/data/di/DataModule.kt | 78 + core/database/.gitignore | 1 + core/database/build.gradle.kts | 44 + core/database/src/main/AndroidManifest.xml | 4 + .../app/fast/database/AccountsDatabase.kt | 16 + .../meloda/app/fast/database/CacheDatabase.kt | 32 + .../app/fast/database/dao/AccountDao.kt | 15 + .../app/fast/database/dao/ConversationDao.kt | 30 + .../meloda/app/fast/database/dao/EntityDao.kt | 20 + .../meloda/app/fast/database/dao/GroupDao.kt | 18 + .../app/fast/database/dao/MessageDao.kt | 24 + .../meloda/app/fast/database/dao/UsersDao.kt | 18 + .../app/fast/database/di/DatabaseModule.kt | 25 + .../database/typeconverters/Converters.kt | 21 + core/datastore/.gitignore | 1 + core/datastore/build.gradle.kts | 34 + core/datastore/src/main/AndroidManifest.xml | 4 + .../meloda/app/fast/datastore/Extensions.kt | 57 + .../app/fast/datastore/SettingsController.kt | 53 + .../meloda/app/fast/datastore/SettingsKeys.kt | 50 + .../meloda/app/fast/datastore/UserSettings.kt | 120 ++ .../app/fast/datastore/di/DataStoreModule.kt | 11 + .../app/fast/datastore/model/ThemeConfig.kt | 11 + core/designsystem/.gitignore | 1 + core/designsystem/build.gradle.kts | 51 + .../designsystem/src/main/AndroidManifest.xml | 4 + .../meloda/app/fast/designsystem/AppTheme.kt | 178 ++ .../meloda/app/fast/designsystem/AutoFill.kt | 121 ++ .../app/fast/designsystem/ContentAlpha.kt | 133 ++ .../app/fast/designsystem/Extensions.kt | 112 ++ .../app/fast/designsystem/ImmutableList.kt | 59 + .../fast/designsystem/LocalContentAlpha.kt | 20 + .../app/fast/designsystem/MaterialDialog.kt | 346 ++++ .../meloda/app/fast/designsystem/TabItem.kt | 7 + .../fast/designsystem}/TextFieldErrorText.kt | 5 +- .../colorschemes/ClassicColorScheme.kt | 155 ++ .../components/BlurrableTopAppBar.kt | 81 + .../components/FullScreenLoader.kt | 21 + .../designsystem/components/NoItemsView.kt | 27 + .../drawable/baseline_account_circle_24.xml | 5 + .../main/res/drawable/baseline_chat_24.xml | 5 + .../res/drawable/baseline_people_alt_24.xml | 11 + .../res/drawable/ic_account_circle_cut.xml | 0 .../src/main/res/drawable/ic_arrow_end.xml | 0 .../main/res/drawable/ic_attachment_audio.xml | 0 .../main/res/drawable/ic_attachment_call.xml | 0 .../main/res/drawable/ic_attachment_file.xml | 0 .../ic_attachment_forwarded_message.xml | 0 .../ic_attachment_forwarded_messages.xml | 0 .../main/res/drawable/ic_attachment_gift.xml | 0 .../res/drawable/ic_attachment_graffiti.xml | 0 .../res/drawable/ic_attachment_group_call.xml | 0 .../main/res/drawable/ic_attachment_link.xml | 0 .../res/drawable/ic_attachment_mini_app.xml | 0 .../main/res/drawable/ic_attachment_photo.xml | 0 .../main/res/drawable/ic_attachment_poll.xml | 0 .../res/drawable/ic_attachment_sticker.xml | 0 .../main/res/drawable/ic_attachment_story.xml | 0 .../main/res/drawable/ic_attachment_video.xml | 0 .../main/res/drawable/ic_attachment_voice.xml | 0 .../main/res/drawable/ic_attachment_wall.xml | 0 .../res/drawable/ic_attachment_wall_reply.xml | 0 .../drawable/ic_baseline_attach_file_24.xml | 0 .../res/drawable/ic_baseline_create_24.xml | 0 .../src/main/res/drawable/ic_fast_logo.xml | 0 .../res/drawable/ic_launcher_foreground.xml | 0 .../src/main/res/drawable/ic_logo_big.xml | 0 .../src/main/res/drawable/ic_map_marker.xml | 0 .../drawable/ic_outline_emoji_emotions_24.xml | 0 .../ic_round_add_circle_outline_24.xml | 0 .../res/drawable/ic_round_arrow_back_24.xml | 0 .../drawable/ic_round_bookmark_border_24.xml | 0 .../main/res/drawable/ic_round_close_24.xml | 0 .../main/res/drawable/ic_round_done_24.xml | 0 .../res/drawable/ic_round_mic_none_24.xml | 0 .../main/res/drawable/ic_round_person_24.xml | 0 .../res/drawable/ic_round_push_pin_24.xml | 0 .../drawable/outline_account_circle_24.xml | 7 + .../src/main/res/drawable/outline_chat_24.xml | 5 + .../res/drawable/outline_people_alt_24.xml | 11 + .../main/res/drawable/pin_off_outline_24.xml | 3 +- .../src/main/res/drawable/pin_outline_24.xml | 4 +- .../res/drawable/round_attach_file_24.xml | 5 + .../src/main/res/drawable/round_cake_24.xml | 0 .../res/drawable/round_delete_outline_24.xml | 9 + .../main/res/drawable/round_done_all_24.xml | 0 .../res/drawable/round_file_download_24.xml | 0 .../res/drawable/round_install_mobile_24.xml | 0 .../main/res/drawable/round_qr_code_24.xml | 0 .../res/drawable/round_restart_alt_24.xml | 0 .../src/main/res/drawable/round_send_24.xml | 12 + .../src/main/res/drawable/round_sms_24.xml | 0 .../main/res/drawable/round_visibility_24.xml | 0 .../res/drawable/round_visibility_off_24.xml | 0 .../main/res/drawable/round_vpn_key_24.xml | 0 .../src/main/res/drawable/test_captcha.webp | Bin .../src/main/res/font/google_sans_bold.ttf | Bin .../main/res/font/google_sans_bold_italic.ttf | Bin .../src/main/res/font/google_sans_italic.ttf | Bin .../src/main/res/font/google_sans_medium.ttf | Bin .../res/font/google_sans_medium_italic.ttf | Bin .../src/main/res/font/google_sans_regular.ttf | Bin .../src/main/res/font/roboto_black.ttf | Bin .../src/main/res/font/roboto_black_italic.ttf | Bin .../src/main/res/font/roboto_bold.ttf | Bin .../src/main/res/font/roboto_bold_italic.ttf | Bin .../src/main/res/font/roboto_italic.ttf | Bin .../src/main/res/font/roboto_light.ttf | Bin .../src/main/res/font/roboto_light_italic.ttf | Bin .../src/main/res/font/roboto_medium.ttf | Bin .../main/res/font/roboto_medium_italic.ttf | Bin .../src/main/res/font/roboto_regular.ttf | Bin .../src/main/res/font/roboto_thin.ttf | Bin .../src/main/res/font/roboto_thin_italic.ttf | Bin .../res/mipmap-anydpi-v26/ic_launcher.xml | 0 .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin .../src/main/res/values-ru/strings.xml | 90 + .../src/main/res/values-uk/strings.xml | 4 +- .../res/values/ic_launcher_background.xml | 0 .../src/main/res/values/plurals.xml | 0 .../src/main/res/values/strings.xml | 77 +- .../src/main/res/values/themes.xml | 5 + core/model/.gitignore | 1 + core/model/build.gradle.kts | 38 + core/model/src/main/AndroidManifest.xml | 4 + .../com/meloda/app/fast/model/ApiEvent.kt | 22 + .../com/meloda/app/fast/model/BaseError.kt | 5 + .../com/meloda/app/fast/model/FriendsInfo.kt | 8 + .../meloda/app/fast/model/InteractionType.kt | 20 + .../meloda/app/fast/model/LongPollEvent.kt | 35 + .../meloda/app/fast/model/api/Extensions.kt | 3 + .../com/meloda/app/fast/model/api/PeerType.kt | 17 + .../app/fast/model/api/data/AttachmentType.kt | 45 + .../fast/model/api/data/LongPollUpdates.kt | 14 + .../app/fast/model/api/data/VkArtistData.kt | 27 + .../fast/model/api/data/VkAttachmentData.kt | 3 + .../model/api/data/VkAttachmentItemData.kt | 35 + .../app/fast/model/api/data/VkAudioData.kt | 56 + .../fast/model/api/data/VkAudioMessageData.kt | 31 + .../model/api/data/VkAudioPlaylistData.kt | 23 + .../app/fast/model/api/data/VkCallData.kt | 25 + .../app/fast/model/api/data/VkChatData.kt | 37 + .../fast/model/api/data/VkChatMemberData.kt | 25 + .../app/fast/model/api/data/VkContactData.kt | 22 + .../fast/model/api/data/VkConversationData.kt | 144 ++ .../app/fast/model/api/data/VkCuratorData.kt | 25 + .../app/fast/model/api/data/VkEventData.kt | 20 + .../app/fast/model/api/data/VkFileData.kt | 60 + .../app/fast/model/api/data/VkGiftData.kt | 21 + .../app/fast/model/api/data/VkGraffitiData.kt | 25 + .../fast/model/api/data/VkGroupCallData.kt | 21 + .../app/fast/model/api/data/VkGroupData.kt | 33 + .../app/fast/model/api/data/VkLinkData.kt | 25 + .../app/fast/model/api/data/VkLongPollData.kt | 12 + .../app/fast/model/api/data/VkMessageData.kt | 208 +++ .../app/fast/model/api/data/VkMiniAppData.kt | 40 + .../app/fast/model/api/data/VkPhotoData.kt | 42 + .../model/api/data/VkPinnedMessageData.kt | 56 + .../app/fast/model/api/data/VkPodcastData.kt | 20 + .../app/fast/model/api/data/VkPollData.kt | 62 + .../app/fast/model/api/data/VkStickerData.kt | 36 + .../app/fast/model/api/data/VkStoryData.kt | 41 + .../app/fast/model/api/data/VkUserData.kt | 59 + .../app/fast/model/api/data/VkVideoData.kt | 75 + .../app/fast/model/api/data/VkWallData.kt | 71 + .../fast/model/api/data/VkWallReplyData.kt | 22 +- .../app/fast/model/api/data/VkWidgetData.kt | 12 + .../fast/model/api/domain/VkArtistDomain.kt | 13 + .../app/fast/model/api/domain/VkAttachment.kt | 7 + .../fast/model/api/domain/VkAudioDomain.kt | 25 + .../model/api/domain/VkAudioMessageDomain.kt | 18 + .../model/api/domain/VkAudioPlaylistDomain.kt | 13 + .../app/fast/model/api/domain/VkCallDomain.kt | 15 + .../app/fast/model/api/domain/VkChatDomain.kt | 21 +- .../model/api/domain/VkChatMemberDomain.kt | 10 + .../fast/model/api/domain/VkContactDomain.kt | 6 + .../fast/model/api/domain/VkConversation.kt | 66 + .../fast/model/api/domain/VkCuratorDomain.kt | 10 + .../fast/model/api/domain/VkEventDomain.kt | 10 + .../app/fast/model/api/domain/VkFileDomain.kt | 29 + .../app/fast/model/api/domain/VkGiftDomain.kt | 13 + .../fast/model/api/domain/VkGraffitiDomain.kt | 15 + .../model/api/domain/VkGroupCallDomain.kt | 10 + .../fast/model/api/domain/VkGroupDomain.kt | 26 + .../app/fast/model/api/domain/VkLinkDomain.kt | 15 + .../app/fast/model/api/domain/VkMessage.kt | 99 ++ .../fast/model/api/domain/VkMiniAppDomain.kt | 10 + .../fast/model/api/domain/VkPhotoDomain.kt | 65 +- .../fast/model/api/domain/VkPodcastDomain.kt | 11 + .../app/fast/model/api/domain/VkPollDomain.kt | 10 + .../fast/model/api/domain/VkStickerDomain.kt | 24 + .../fast/model/api/domain/VkStoryDomain.kt | 13 + .../app/fast/model/api/domain/VkUser.kt | 44 + .../fast/model/api/domain/VkVideoDomain.kt | 50 +- .../app/fast/model/api/domain/VkWallDomain.kt | 22 + .../model/api/domain/VkWallReplyDomain.kt | 10 + .../fast/model/api/domain/VkWidgetDomain.kt | 10 + .../model/api/requests}/AccountRequests.kt | 6 +- .../api/requests}/ConversationsRequest.kt | 19 +- .../fast/model/api/requests/FriendsRequest.kt | 33 + .../model/api/requests}/LongPollRequests.kt | 11 +- .../model/api/requests}/MessagesRequest.kt | 158 +- .../fast/model/api/requests/OAuthRequest.kt | 24 +- .../fast/model/api/requests/PhotosRequests.kt | 14 + .../fast/model/api/requests}/UsersRequest.kt | 15 +- .../model/api/responses/AudiosResponses.kt | 16 + .../fast/model/api/responses/AuthResponse.kt | 12 + .../api/responses/ConversationsResponse.kt | 30 + .../model/api/responses/FilesResponses.kt | 20 + .../model/api/responses/FriendsResponse.kt | 11 + .../model/api/responses/MessagesResponse.kt | 35 + .../fast/model/api/responses/OAuthResponse.kt | 37 + .../model/api/responses/PhotosResponses.kt | 16 + .../model/api/responses/VideosResponses.kt | 21 + .../app/fast/model/database/AccountEntity.kt | 12 +- .../model/database/ConversationWithMessage.kt | 13 + .../model/database/VkConversationEntity.kt | 64 + .../app/fast/model/database/VkGroupEntity.kt | 15 + .../fast/model/database/VkMessageEntity.kt | 54 + .../app/fast/model/database/VkUserEntity.kt | 20 + core/network/.gitignore | 1 + core/network/build.gradle.kts | 54 + core/network/src/main/AndroidManifest.xml | 4 + .../meloda/app/fast/network/ApiResponse.kt | 15 + .../com/meloda/app/fast/network/Extensions.kt | 126 ++ .../meloda/app/fast/network/JsonConverter.kt | 10 + .../meloda/app/fast/network/MoshiConverter.kt | 17 + .../com/meloda/app/fast/network/OAuthError.kt | 141 ++ .../app/fast/network/OAuthErrorDomain.kt | 31 + .../meloda/app/fast/network/OAuthResponse.kt | 8 + .../fast/network/OAuthResultCallFactory.kt | 196 +++ .../fast/network/ResponseConverterFactory.kt | 74 + .../meloda/app/fast/network/RestApiError.kt | 17 + .../app/fast/network/RestApiErrorDomain.kt | 6 + .../meloda/app/fast/network/ValidationType.kt | 9 + .../meloda/app/fast/network/VkErrorCodes.kt | 60 + .../app/fast/network/di/NetworkModule.kt | 94 + .../network/service/account/AccountService.kt | 21 + .../network/service/account/AccountUrls.kt | 8 + .../network/service/audios/AudiosService.kt | 32 + .../fast/network/service/audios/AudiosUrls.kt | 10 + .../fast/network/service/auth/AuthService.kt | 14 + .../app/fast/network/service/auth/AuthUrls.kt | 8 + .../conversations/ConversationsService.kt | 37 + .../conversations/ConversationsUrls.kt | 12 + .../network/service/files/FilesService.kt | 38 + .../fast/network/service/files/FilesUrls.kt | 10 + .../network/service/friends/FriendsService.kt | 24 + .../network/service/friends/FriendsUrls.kt | 9 + .../service/longpoll/LongPollService.kt | 17 + .../service/messages/MessagesService.kt | 93 + .../network/service/messages/MessagesUrls.kt | 21 + .../network/service/oauth/OAuthService.kt | 16 + .../fast/network/service/oauth/OAuthUrls.kt | 8 + .../fast/network/service/photos/PhotoUrls.kt | 10 + .../network/service/photos/PhotosService.kt | 37 + .../network/service/users/UsersService.kt | 18 + .../fast/network/service/users/UsersUrls.kt | 8 + .../network/service/videos/VideosService.kt | 25 + .../fast/network/service/videos/VideosUrls.kt | 8 + core/ui/.gitignore | 1 + core/ui/build.gradle.kts | 40 + core/ui/src/main/AndroidManifest.xml | 4 + .../com/meloda/app/fast/ui/ErrorView.kt | 42 + feature/auth/.gitignore | 1 + feature/auth/build.gradle.kts | 92 + feature/auth/captcha/.gitignore | 1 + feature/auth/captcha/build.gradle.kts | 59 + feature/auth/captcha/consumer-rules.pro | 0 feature/auth/captcha/proguard-rules.pro | 21 + .../auth/captcha/src/main/AndroidManifest.xml | 4 + .../fast/auth/captcha}/CaptchaViewModel.kt | 62 +- .../app/fast/auth/captcha/di/CaptchaDI.kt | 14 + .../auth/captcha/model/CaptchaArguments.kt | 12 + .../auth}/captcha/model/CaptchaScreenState.kt | 8 +- .../captcha/model/CaptchaValidationResult.kt | 8 + .../auth/captcha/navigation/CaptchaRoute.kt | 48 + .../captcha/presentation/CaptchaScreen.kt | 234 +++ .../captcha/validation/CaptchaValidator.kt | 6 +- feature/auth/login/.gitignore | 1 + feature/auth/login/build.gradle.kts | 92 + feature/auth/login/consumer-rules.pro | 0 feature/auth/login/proguard-rules.pro | 21 + .../auth/login/src/main/AndroidManifest.xml | 4 + .../meloda/fast/auth/login/LoginViewModel.kt | 376 ++++ .../meloda/fast/auth/login/OAuthUseCase.kt | 17 + .../fast/auth/login/OAuthUseCaseImpl.kt | 111 ++ .../meloda/fast/auth/login/di/LoginModule.kt | 17 + .../meloda/fast/auth/login/model/AuthInfo.kt | 7 + .../fast/auth/login/model/CaptchaArguments.kt | 12 + .../fast/auth/login/model/LoginArguments.kt | 8 + .../fast/auth/login/model/LoginError.kt | 6 + .../auth}/login/model/LoginScreenState.kt | 26 +- .../auth/login/model/LoginTwoFaArguments.kt | 16 + .../auth/login/model/LoginValidationResult.kt | 12 + .../auth/login/model/UserBannedArguments.kt | 14 + .../fast/auth/login/navigation/LoginRoute.kt | 56 + .../auth/login/presentation/LoginScreen.kt | 330 ++++ .../auth/login/presentation/LogoScreen.kt | 110 ++ .../auth}/login/validation/LoginValidator.kt | 8 +- feature/auth/src/main/AndroidManifest.xml | 4 + .../com/meloda/app/fast/auth/AuthGraph.kt | 102 ++ .../com/meloda/app/fast/auth/AuthModule.kt | 14 + feature/auth/twofa/.gitignore | 1 + feature/auth/twofa/build.gradle.kts | 60 + feature/auth/twofa/consumer-rules.pro | 0 feature/auth/twofa/proguard-rules.pro | 21 + .../auth/twofa/src/main/AndroidManifest.xml | 4 + .../meloda/app/fast/auth/twofa/AuthUseCase.kt | 12 + .../app/fast/auth/twofa/AuthUseCaseImpl.kt | 27 + .../app/fast/auth/twofa/TwoFaViewModel.kt | 212 +++ .../app/fast/auth/twofa/di/TwoFaModule.kt | 17 + .../fast/auth/twofa/model/TwoFaArguments.kt | 16 + .../fast/auth/twofa/model/TwoFaScreenState.kt | 28 + .../fast/auth/twofa/model/TwoFaUiAction.kt | 6 + .../auth/twofa/model/TwoFaValidationResult.kt | 8 + .../auth}/twofa/model/TwoFaValidationType.kt | 15 +- .../fast/auth/twofa/navigation/TwoFaRoute.kt | 49 + .../auth/twofa/presentation/TwoFaScreen.kt | 219 ++- .../auth/twofa/validation/TwoFaValidator.kt | 14 + feature/auth/userbanned/.gitignore | 1 + feature/auth/userbanned/build.gradle.kts | 60 + .../userbanned/src/main/AndroidManifest.xml | 4 + .../userbanned/model/UserBannedArguments.kt | 14 + .../userbanned/navigation/UserBannedRoute.kt | 53 + .../presentation/UserBannedScreen.kt | 97 ++ feature/chatmaterials/.gitignore | 1 + feature/chatmaterials/build.gradle.kts | 62 + .../src/main/AndroidManifest.xml | 4 + .../ChatMaterialsScreenContent.kt | 91 + .../navigation/ChatMaterialsRoute.kt | 24 + feature/conversations/.gitignore | 1 + feature/conversations/build.gradle.kts | 69 + .../src/main/AndroidManifest.xml | 4 + .../conversations/ConversationsViewModel.kt | 625 +++++++ .../com/meloda/app/fast/conversations/Dots.kt | 342 ++++ .../data/ConversationsUseCaseImpl.kt | 119 ++ .../conversations/di/ConversationsModule.kt | 15 + .../fast/conversations/model/ActionState.kt | 20 + .../conversations/model/ConversationOption.kt | 31 + .../model/ConversationsScreenState.kt | 23 + .../model/ConversationsShowOptions.kt | 14 + .../conversations/model/UiConversation.kt | 31 + .../navigation/ConversationRoute.kt | 33 + .../presentation/ConversationItem.kt | 392 +++++ .../presentation/ConversationsList.kt | 109 ++ .../presentation/ConversationsScreen.kt | 441 +++++ .../util/ConversationDomainMapper.kt | 848 +++++++++ feature/friends/.gitignore | 1 + feature/friends/build.gradle.kts | 69 + feature/friends/consumer-rules.pro | 0 feature/friends/proguard-rules.pro | 21 + feature/friends/src/main/AndroidManifest.xml | 4 + .../app/fast/friends/FriendsViewModel.kt | 175 ++ .../app/fast/friends/di/FriendsModule.kt | 15 + .../fast/friends/domain/FriendsUseCaseImpl.kt | 42 + .../fast/friends/model/FriendsScreenState.kt | 21 + .../app/fast/friends/model/OnlineState.kt | 5 + .../meloda/app/fast/friends/model/UiFriend.kt | 11 + .../fast/friends/navigation/FriendsRoute.kt | 29 + .../fast/friends/presentation/FriendItem.kt | 100 ++ .../fast/friends/presentation/FriendsList.kt | 95 + .../friends/presentation/FriendsScreen.kt | 308 ++++ .../app/fast/friends/util/FriendMapper.kt | 19 + feature/languagepicker/.gitignore | 1 + feature/languagepicker/build.gradle.kts | 59 + .../src/main/AndroidManifest.xml | 4 + .../languagepicker/LanguagePickerViewModel.kt | 76 + .../languagepicker/di/LanguagePickerModule.kt | 9 + .../model/LanguagePickerScreenState.kt | 9 + .../model/SelectableLanguage.kt | 8 + .../navigation/LanguagePickerRoute.kt | 78 + .../presentation/LanguagePickerScreen.kt | 232 +++ feature/messageshistory/.gitignore | 1 + feature/messageshistory/build.gradle.kts | 71 + .../src/main/AndroidManifest.xml | 4 + .../MessagesHistoryViewModel.kt | 871 ++++++++++ .../di/MessagesHistoryModule.kt | 17 + .../domain/MessagesUseCaseImpl.kt | 130 ++ .../fast/messageshistory/model/ActionMode.kt | 9 + .../model/MessagesHistoryArguments.kt | 9 + .../model/MessagesHistoryScreenState.kt | 37 + .../model/MessagesHistoryValidationResult.kt | 9 + .../fast/messageshistory/model/UiMessage.kt | 19 + .../navigation/MessagesHistoryRoute.kt | 64 + .../presentation/IncomingMessageBubble.kt | 86 + .../presentation/MessageBubble.kt | 81 + .../presentation/MessagesHistoryScreen.kt | 448 +++++ .../presentation/MessagesList.kt | 119 ++ .../presentation/OutgoingMessageBubble.kt | 54 + .../messageshistory/util/MessageMapper.kt | 116 ++ .../validation/MessagesHistoryValidator.kt | 30 + feature/photoviewer/.gitignore | 1 + feature/photoviewer/build.gradle.kts | 55 + .../photoviewer/src/main/AndroidManifest.xml | 4 + .../fast/photoviewer/PhotoViewViewModel.kt | 22 + .../app/fast/photoviewer/di/PhotoViewDI.kt | 11 + .../photoviewer/model/PhotoViewArguments.kt | 9 + .../fast/photoviewer/model/PhotoViewState.kt | 16 + .../presentation/PhotoViewScreenContent.kt | 180 ++ feature/profile/.gitignore | 1 + feature/profile/build.gradle.kts | 71 + feature/profile/consumer-rules.pro | 0 feature/profile/proguard-rules.pro | 21 + feature/profile/src/main/AndroidManifest.xml | 4 + .../app/fast/profile/ProfileViewModel.kt | 12 + .../app/fast/profile/di/ProfileModule.kt | 9 + feature/settings/.gitignore | 1 + feature/settings/build.gradle.kts | 63 + feature/settings/src/main/AndroidManifest.xml | 4 + .../app/fast/settings/SettingsViewModel.kt | 422 +++++ .../app/fast/settings/di/SettingsModule.kt | 11 + .../app/fast}/settings/model/SettingsItem.kt | 89 +- .../fast}/settings/model/SettingsListeners.kt | 4 +- .../settings/model/SettingsScreenState.kt | 25 + .../settings/model/SettingsShowOptions.kt | 16 + .../settings/presentation/SettingsRoute.kt | 30 + .../settings/presentation/SettingsScreen.kt | 410 +++++ .../presentation/items/ListSettingsItem.kt | 154 ++ .../presentation}/items/SwitchSettingsItem.kt | 79 +- .../items/TextFieldSettingsItem.kt | 186 ++ .../presentation}/items/TitleSettingsItem.kt | 28 +- .../items/TitleSummarySettingsItem.kt | 102 ++ gradle.properties | 5 +- gradle/libs.versions.toml | 103 ++ gradle/wrapper/gradle-wrapper.properties | 4 +- settings.gradle.kts | 39 + 906 files changed, 23577 insertions(+), 24115 deletions(-) create mode 100644 .github/workflows/android_dev.yml create mode 100644 .github/workflows/android_master.yml create mode 100644 app/keystore/keystore.jks create mode 100644 app/schemas/com.meloda.fast.database.AccountsDatabase/2.json create mode 100644 app/schemas/com.meloda.fast.database.CacheDatabase/3.json create mode 100644 app/src/androidTest/kotlin/com/meloda/fast/tests/LoginSignInTest.kt create mode 100644 app/src/debug/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/debug/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/debug/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/debug/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png rename app/src/{dev/res/values/strings.xml => debug/res/values/ic_launcher_background.xml} (52%) create mode 100644 app/src/debug/res/values/strings.xml delete mode 100644 app/src/dev/res/mipmap-hdpi/ic_launcher.png delete mode 100644 app/src/dev/res/mipmap-mdpi/ic_launcher.png delete mode 100644 app/src/dev/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 app/src/dev/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/kotlin/com/meloda/app/fast/MainActivity.kt create mode 100644 app/src/main/kotlin/com/meloda/app/fast/MainGraph.kt create mode 100644 app/src/main/kotlin/com/meloda/app/fast/MainViewModel.kt create mode 100644 app/src/main/kotlin/com/meloda/app/fast/RootGraph.kt create mode 100644 app/src/main/kotlin/com/meloda/app/fast/common/AppGlobal.kt create mode 100644 app/src/main/kotlin/com/meloda/app/fast/common/di/ApplicationModule.kt create mode 100644 app/src/main/kotlin/com/meloda/app/fast/model/LongPollState.kt create mode 100644 app/src/main/kotlin/com/meloda/app/fast/model/MainScreenState.kt create mode 100644 app/src/main/kotlin/com/meloda/app/fast/model/ServicesState.kt rename app/src/main/kotlin/com/meloda/{ => app}/fast/receiver/DownloadManagerReceiver.kt (89%) create mode 100644 app/src/main/kotlin/com/meloda/app/fast/service/OnlineService.kt create mode 100644 app/src/main/kotlin/com/meloda/app/fast/service/longpolling/LongPollingService.kt create mode 100644 app/src/main/kotlin/com/meloda/app/fast/service/longpolling/di/LongPollModule.kt rename app/src/main/kotlin/com/meloda/{ => app}/fast/util/NotificationsUtils.kt (93%) delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/ApiEvent.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/ApiExtensions.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/base/ApiError.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/base/ApiResponse.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/base/AttachmentClassNameIsEmptyException.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/longpoll/LongPollEvent.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/longpoll/LongPollUpdatesParser.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/ActionState.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/ConversationPeerType.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/VkChatMember.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/VkGroup.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/VkMessage.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/VkUser.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAttachment.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAudio.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCall.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCurator.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkEvent.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkFile.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGift.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGraffiti.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGroupCall.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkLink.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkMiniApp.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPoll.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkSticker.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkStory.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVoiceMessage.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWall.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWallReply.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWidget.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkChat.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkChatMember.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkGroup.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkLongPoll.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkMessage.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkUser.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAttachmentItem.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAudio.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCall.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCurator.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkEvent.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkFile.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGift.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGraffiti.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGroupCall.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkLink.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkMiniApp.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPhoto.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPoll.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkSticker.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkStory.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVideo.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVoiceMessage.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWall.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWidget.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/data/BaseVkConversation.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/domain/VkConversationDomain.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/presentation/VkConversationUi.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/ApiErrors.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/ResultCallFactory.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/account/AccountUrls.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosRequests.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosResponses.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosUrls.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthResponse.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthUrls.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsResponse.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsUrls.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/files/FileRequests.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/files/FilesResponses.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/files/FilesUrls.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesResponse.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesUrls.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/ota/OtaResponses.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/ota/OtaUrls.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotoUrls.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotosRequests.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotosResponses.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/users/UsersResponse.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/users/UsersUrls.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosRequests.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosResponses.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosUrls.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/base/BaseActivity.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/base/BaseFragment.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/base/ResourceProvider.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/base/adapter/AsyncDiffItemAdapter.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/base/adapter/BaseAdapter.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/base/adapter/Holders.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/base/adapter/Listeners.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/base/screen/AppScreen.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModelFragment.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/base/viewmodel/DeprecatedBaseViewModel.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/base/viewmodel/ErrorHandler.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/base/viewmodel/Events.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/base/viewmodel/ViewModelUtils.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/common/AppConstants.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/common/AppGlobal.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/common/Screens.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/common/UpdateManager.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/common/di/ApplicationModule.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/compose/Dialogs.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/account/AccountApi.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/account/AccountsDao.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/account/AccountsRepository.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/audios/AudiosApi.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/audios/AudiosRepository.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/auth/AuthApi.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/auth/AuthRepository.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsApi.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsDao.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsRepository.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/files/FilesApi.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/files/FilesRepository.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/groups/GroupsDao.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/groups/GroupsRepository.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/longpoll/LongPollApi.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/messages/MessagesApi.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/messages/MessagesDao.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/messages/MessagesRepository.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/ota/OtaApi.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/photos/PhotosApi.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/photos/PhotosRepository.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/users/UsersApi.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/users/UsersDao.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/users/UsersRepository.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/videos/VideosApi.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/data/videos/VideosRepository.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/database/AccountsDatabase.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/database/CacheDatabase.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/database/Converters.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/di/ApiModule.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/di/DataModule.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/di/DatabaseModule.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/di/NavigationModule.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/di/OtaModule.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ext/ActivityExt.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ext/AndroidVersionsExt.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ext/BooleanExt.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ext/BundleExt.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ext/ComposeExt.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ext/ContextExt.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ext/Ext.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ext/FragmentExt.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ext/GlideExt.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ext/NumbersExt.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ext/StringExt.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ext/ViewExt.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/model/SelectableItem.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/model/UpdateItem.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/model/base/AdapterDiffItem.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/model/base/DisplayableItem.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/model/base/UiImage.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/receiver/StopLongPollServiceReceiver.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/captcha/CaptchaScreens.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/captcha/di/CaptchaDI.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/captcha/model/CaptchaValidationResult.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/captcha/presentation/CaptchaCoordinator.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/captcha/presentation/CaptchaFragment.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaArguments.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaResult.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaScreen.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoFragment.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoMembersAdapter.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoMembersFragment.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoPagerAdapter.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoViewModel.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/chatinfo/di/ChatInfoDI.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationCompose.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsResourceProvider.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/conversations/adapter/ConversationsDelegates.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/conversations/di/ConversationsModule.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/login/LoginScreens.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/login/di/LoginModule.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/login/model/LoginResult.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/login/model/LoginValidationResult.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/login/screen/LoginScreen.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/main/MainViewModel.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/main/activity/LongPollState.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/main/activity/LongPollUtils.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/main/activity/MainActivity.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/main/activity/ServicesState.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/main/di/MainModule.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentsAdapter.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/messages/ForwardedMessagesFragment.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/messages/di/MessagesHistoryModule.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewFragment.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewViewModel.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/photos/di/PhotoViewDI.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/settings/SettingsFragment.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/settings/SettingsViewModel.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/settings/di/SettingsModule.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/settings/items/ListSettingsItem.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/settings/items/TextFieldSettingsItem.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/settings/items/TitleSummarySettingsItem.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/testing/TestActivity.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/twofa/TwoFaScreens.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/twofa/di/TwoFaDI.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaArguments.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaResult.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaScreenState.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaValidationResult.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/twofa/presentation/TwoFaViewModel.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/twofa/screen/TwoFaCoordinator.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/twofa/screen/TwoFaScreen.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/twofa/validation/TwoFaValidator.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/updates/UpdatesFragment.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/updates/UpdatesViewModel.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/updates/di/UpdatesDI.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/updates/model/UpdateState.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/updates/model/UpdatesScreenState.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/screens/userbanned/UserBannedFragment.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/service/LongPollQSTileService.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/service/LongPollService.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/service/MyCustomControlService.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/service/OnlineService.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ui/AppTheme.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ui/BlueColorScheme.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ui/GreenColorScheme.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ui/RedColorScheme.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ui/colors/Blue.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ui/colors/Green.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ui/colors/Red.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/ui/widgets/AsyncImage.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/util/ColorUtils.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/util/TimeUtils.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/view/BoundedLinearLayout.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/view/CircleImageView.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/view/DialogToolbar.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/view/SpaceItemDecoration.kt delete mode 100644 app/src/main/res/anim/activity_close_enter.xml delete mode 100644 app/src/main/res/anim/activity_close_exit.xml delete mode 100644 app/src/main/res/anim/activity_open_enter.xml delete mode 100644 app/src/main/res/anim/activity_open_exit.xml delete mode 100644 app/src/main/res/anim/fast_out_extra_slow_in.xml delete mode 100644 app/src/main/res/drawable-hdpi/ic_notification_new_message.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_notification_new_message.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_notification_new_message.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_notification_new_message.png delete mode 100644 app/src/main/res/drawable/ic_back.xml delete mode 100644 app/src/main/res/drawable/ic_baseline_account_circle_24.xml delete mode 100644 app/src/main/res/drawable/ic_chat_attachment_panel_background.xml delete mode 100644 app/src/main/res/drawable/ic_close_in_circle.xml delete mode 100644 app/src/main/res/drawable/ic_image_button_circle_background.xml delete mode 100644 app/src/main/res/drawable/ic_key.xml delete mode 100644 app/src/main/res/drawable/ic_message_attachment_story_image_dimmer.xml delete mode 100644 app/src/main/res/drawable/ic_message_in_background.xml delete mode 100644 app/src/main/res/drawable/ic_message_in_background_middle.xml delete mode 100644 app/src/main/res/drawable/ic_message_out_background.xml delete mode 100644 app/src/main/res/drawable/ic_message_out_background_middle.xml delete mode 100644 app/src/main/res/drawable/ic_message_out_background_middle_stroke.xml delete mode 100644 app/src/main/res/drawable/ic_message_out_background_stroke.xml delete mode 100644 app/src/main/res/drawable/ic_message_panel_background.xml delete mode 100644 app/src/main/res/drawable/ic_message_panel_gradient.xml delete mode 100644 app/src/main/res/drawable/ic_message_unread.xml delete mode 100644 app/src/main/res/drawable/ic_messages_history_toolbar_gradient_background.xml delete mode 100644 app/src/main/res/drawable/ic_notification_new_message.xml delete mode 100644 app/src/main/res/drawable/ic_online_pc.xml delete mode 100644 app/src/main/res/drawable/ic_people_outline.xml delete mode 100644 app/src/main/res/drawable/ic_phantom.xml delete mode 100644 app/src/main/res/drawable/ic_play_button_circle_background.xml delete mode 100644 app/src/main/res/drawable/ic_round_access_time_24.xml delete mode 100644 app/src/main/res/drawable/ic_round_close_20.xml delete mode 100644 app/src/main/res/drawable/ic_round_emoji_emotions_24.xml delete mode 100644 app/src/main/res/drawable/ic_round_error_outline_24.xml delete mode 100644 app/src/main/res/drawable/ic_round_group_24.xml delete mode 100644 app/src/main/res/drawable/ic_round_keyboard_arrow_down_24.xml delete mode 100644 app/src/main/res/drawable/ic_round_link_24.xml delete mode 100644 app/src/main/res/drawable/ic_round_mail_24.xml delete mode 100644 app/src/main/res/drawable/ic_round_mic_24.xml delete mode 100644 app/src/main/res/drawable/ic_round_play_arrow_24.xml delete mode 100644 app/src/main/res/drawable/ic_round_send_24.xml delete mode 100644 app/src/main/res/drawable/ic_round_settings_24.xml delete mode 100644 app/src/main/res/drawable/ic_round_settings_primary.xml delete mode 100644 app/src/main/res/drawable/ic_round_star_24.xml delete mode 100644 app/src/main/res/drawable/ic_search.xml delete mode 100644 app/src/main/res/drawable/ic_security.xml delete mode 100644 app/src/main/res/drawable/ic_star_border.xml delete mode 100644 app/src/main/res/drawable/time_read_indicator_on_attachments_background.xml delete mode 100644 app/src/main/res/font/tt_commons_bold.ttf delete mode 100644 app/src/main/res/font/tt_commons_medium.ttf delete mode 100644 app/src/main/res/font/tt_commons_regular.ttf delete mode 100644 app/src/main/res/layout/activity_main.xml delete mode 100644 app/src/main/res/layout/dialog_captcha.xml delete mode 100644 app/src/main/res/layout/dialog_fast_login.xml delete mode 100644 app/src/main/res/layout/dialog_message_delete.xml delete mode 100644 app/src/main/res/layout/dialog_validation.xml delete mode 100644 app/src/main/res/layout/drawer_header.xml delete mode 100644 app/src/main/res/layout/fragment_chat_info.xml delete mode 100644 app/src/main/res/layout/fragment_chat_info_members.xml delete mode 100644 app/src/main/res/layout/fragment_conversations.xml delete mode 100644 app/src/main/res/layout/fragment_forwarded_messages.xml delete mode 100644 app/src/main/res/layout/fragment_messages_history.xml delete mode 100644 app/src/main/res/layout/fragment_settings.xml delete mode 100644 app/src/main/res/layout/fragment_updates.xml delete mode 100644 app/src/main/res/layout/fragment_user_banned.xml delete mode 100644 app/src/main/res/layout/item_chat_member.xml delete mode 100644 app/src/main/res/layout/item_conversation.xml delete mode 100644 app/src/main/res/layout/item_message_attachment_audio.xml delete mode 100644 app/src/main/res/layout/item_message_attachment_call.xml delete mode 100644 app/src/main/res/layout/item_message_attachment_file.xml delete mode 100644 app/src/main/res/layout/item_message_attachment_forwards.xml delete mode 100644 app/src/main/res/layout/item_message_attachment_geo.xml delete mode 100644 app/src/main/res/layout/item_message_attachment_gift.xml delete mode 100644 app/src/main/res/layout/item_message_attachment_graffiti.xml delete mode 100644 app/src/main/res/layout/item_message_attachment_link.xml delete mode 100644 app/src/main/res/layout/item_message_attachment_photo.xml delete mode 100644 app/src/main/res/layout/item_message_attachment_reply.xml delete mode 100644 app/src/main/res/layout/item_message_attachment_sticker.xml delete mode 100644 app/src/main/res/layout/item_message_attachment_story.xml delete mode 100644 app/src/main/res/layout/item_message_attachment_video.xml delete mode 100644 app/src/main/res/layout/item_message_attachment_voice.xml delete mode 100644 app/src/main/res/layout/item_message_attachment_wall_post.xml delete mode 100644 app/src/main/res/layout/item_message_in.xml delete mode 100644 app/src/main/res/layout/item_message_out.xml delete mode 100644 app/src/main/res/layout/item_message_service.xml delete mode 100644 app/src/main/res/layout/item_settings_checkbox.xml delete mode 100644 app/src/main/res/layout/item_settings_edit_text.xml delete mode 100644 app/src/main/res/layout/item_settings_edit_text_alert.xml delete mode 100644 app/src/main/res/layout/item_settings_list.xml delete mode 100644 app/src/main/res/layout/item_settings_switch.xml delete mode 100644 app/src/main/res/layout/item_settings_title.xml delete mode 100644 app/src/main/res/layout/item_settings_title_summary.xml delete mode 100644 app/src/main/res/layout/item_uploaded_attachment_audio.xml delete mode 100644 app/src/main/res/layout/item_uploaded_attachment_file.xml delete mode 100644 app/src/main/res/layout/item_uploaded_attachment_photo.xml delete mode 100644 app/src/main/res/layout/item_uploaded_attachment_video.xml delete mode 100644 app/src/main/res/layout/toolbar_menu_item_avatar.xml delete mode 100644 app/src/main/res/layout/view_dialog_toolbar.xml delete mode 100644 app/src/main/res/menu/activity_main_bottom.xml delete mode 100644 app/src/main/res/menu/activity_main_drawer.xml delete mode 100644 app/src/main/res/menu/fragment_conversations.xml delete mode 100644 app/src/main/res/menu/fragment_conversations_popup.xml delete mode 100644 app/src/main/res/values-night/bools.xml delete mode 100644 app/src/main/res/values-night/colors.xml delete mode 100644 app/src/main/res/values-ru/strings.xml delete mode 100644 app/src/main/res/values-v27/themes.xml delete mode 100644 app/src/main/res/values-v31/monet_colors.xml delete mode 100644 app/src/main/res/values/attrs.xml delete mode 100644 app/src/main/res/values/bools.xml delete mode 100644 app/src/main/res/values/colors.xml delete mode 100644 app/src/main/res/values/dimens.xml delete mode 100644 app/src/main/res/values/monet_colors.xml delete mode 100644 app/src/main/res/values/styles.xml delete mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/network_security_config.xml delete mode 100644 app/src/main/res/xml/preferences.xml delete mode 100644 app/src/main/res/xml/shortcuts.xml create mode 100644 app/src/staging/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/staging/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/staging/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/staging/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/staging/res/mipmap-xxxhdpi/ic_launcher.png rename app/src/{dev => staging}/res/values/ic_launcher_background.xml (100%) create mode 100644 app/src/staging/res/values/strings.xml create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/settings.gradle.kts create mode 100644 buildSrc/src/main/kotlin/Configs.kt create mode 100644 core/common/.gitignore create mode 100644 core/common/build.gradle.kts create mode 100644 core/common/src/main/AndroidManifest.xml create mode 100644 core/common/src/main/kotlin/com/meloda/app/fast/common/AppConstants.kt create mode 100644 core/common/src/main/kotlin/com/meloda/app/fast/common/AuthInterceptor.kt create mode 100644 core/common/src/main/kotlin/com/meloda/app/fast/common/CustomNavType.kt create mode 100644 core/common/src/main/kotlin/com/meloda/app/fast/common/UiImage.kt rename {app/src/main/kotlin/com/meloda/fast/model/base => core/common/src/main/kotlin/com/meloda/app/fast/common}/UiText.kt (52%) rename {app/src/main/kotlin/com/meloda/fast/api => core/common/src/main/kotlin/com/meloda/app/fast/common}/UserConfig.kt (56%) rename app/src/main/kotlin/com/meloda/fast/api/VKConstants.kt => core/common/src/main/kotlin/com/meloda/app/fast/common/VkConstants.kt (61%) create mode 100644 core/common/src/main/kotlin/com/meloda/app/fast/common/di/CommonModule.kt create mode 100644 core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/Extensions.kt create mode 100644 core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/StringsExtensions.kt create mode 100644 core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/navigation/NavigationExtensions.kt rename {app/src/main/kotlin/com/meloda/fast => core/common/src/main/kotlin/com/meloda/app/fast/common}/util/AndroidUtils.kt (72%) create mode 100644 core/common/src/main/kotlin/com/meloda/app/fast/common/util/TimeUtils.kt create mode 100644 core/common/src/main/kotlin/com/meloda/app/fast/common/util/VkUtils.kt create mode 100644 core/common/src/main/res/values/strings.xml create mode 100644 core/data/.gitignore create mode 100644 core/data/build.gradle.kts create mode 100644 core/data/src/main/AndroidManifest.xml create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUpdatesParser.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUseCase.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUseCaseImpl.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/State.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/VkGroupsMap.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/VkMemoryCache.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/VkUsersMap.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountRepository.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountRepositoryImpl.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountUseCase.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountUseCaseImpl.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/audios/AudiosRepository.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/auth/AuthRepository.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/auth/AuthRepositoryImpl.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsRepository.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsRepositoryImpl.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsUseCase.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/files/FilesRepository.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsRepository.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsRepositoryImpl.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsUseCase.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/longpoll/LongPollRepository.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/longpoll/LongPollRepositoryImpl.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSource.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSourceImpl.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSource.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSourceImpl.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepository.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepositoryImpl.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesUseCase.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepository.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepositoryImpl.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/photos/PhotosRepository.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepository.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepositoryImpl.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCase.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCaseImpl.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/api/videos/VideosRepository.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepository.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepositoryImpl.kt create mode 100644 core/data/src/main/kotlin/com/meloda/app/fast/data/di/DataModule.kt create mode 100644 core/database/.gitignore create mode 100644 core/database/build.gradle.kts create mode 100644 core/database/src/main/AndroidManifest.xml create mode 100644 core/database/src/main/kotlin/com/meloda/app/fast/database/AccountsDatabase.kt create mode 100644 core/database/src/main/kotlin/com/meloda/app/fast/database/CacheDatabase.kt create mode 100644 core/database/src/main/kotlin/com/meloda/app/fast/database/dao/AccountDao.kt create mode 100644 core/database/src/main/kotlin/com/meloda/app/fast/database/dao/ConversationDao.kt create mode 100644 core/database/src/main/kotlin/com/meloda/app/fast/database/dao/EntityDao.kt create mode 100644 core/database/src/main/kotlin/com/meloda/app/fast/database/dao/GroupDao.kt create mode 100644 core/database/src/main/kotlin/com/meloda/app/fast/database/dao/MessageDao.kt create mode 100644 core/database/src/main/kotlin/com/meloda/app/fast/database/dao/UsersDao.kt create mode 100644 core/database/src/main/kotlin/com/meloda/app/fast/database/di/DatabaseModule.kt create mode 100644 core/database/src/main/kotlin/com/meloda/app/fast/database/typeconverters/Converters.kt create mode 100644 core/datastore/.gitignore create mode 100644 core/datastore/build.gradle.kts create mode 100644 core/datastore/src/main/AndroidManifest.xml create mode 100644 core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/Extensions.kt create mode 100644 core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsController.kt create mode 100644 core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsKeys.kt create mode 100644 core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/UserSettings.kt create mode 100644 core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/di/DataStoreModule.kt create mode 100644 core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/model/ThemeConfig.kt create mode 100644 core/designsystem/.gitignore create mode 100644 core/designsystem/build.gradle.kts create mode 100644 core/designsystem/src/main/AndroidManifest.xml create mode 100644 core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AppTheme.kt create mode 100644 core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AutoFill.kt create mode 100644 core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/ContentAlpha.kt create mode 100644 core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/Extensions.kt create mode 100644 core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/ImmutableList.kt create mode 100644 core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/LocalContentAlpha.kt create mode 100644 core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/MaterialDialog.kt create mode 100644 core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/TabItem.kt rename {app/src/main/kotlin/com/meloda/fast/ui/widgets => core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem}/TextFieldErrorText.kt (83%) create mode 100644 core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/colorschemes/ClassicColorScheme.kt create mode 100644 core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/BlurrableTopAppBar.kt create mode 100644 core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/FullScreenLoader.kt create mode 100644 core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/NoItemsView.kt create mode 100644 core/designsystem/src/main/res/drawable/baseline_account_circle_24.xml create mode 100644 core/designsystem/src/main/res/drawable/baseline_chat_24.xml create mode 100644 core/designsystem/src/main/res/drawable/baseline_people_alt_24.xml rename {app => core/designsystem}/src/main/res/drawable/ic_account_circle_cut.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_arrow_end.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_attachment_audio.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_attachment_call.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_attachment_file.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_attachment_forwarded_message.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_attachment_forwarded_messages.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_attachment_gift.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_attachment_graffiti.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_attachment_group_call.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_attachment_link.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_attachment_mini_app.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_attachment_photo.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_attachment_poll.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_attachment_sticker.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_attachment_story.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_attachment_video.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_attachment_voice.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_attachment_wall.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_attachment_wall_reply.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_baseline_attach_file_24.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_baseline_create_24.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_fast_logo.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_launcher_foreground.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_logo_big.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_map_marker.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_outline_emoji_emotions_24.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_round_add_circle_outline_24.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_round_arrow_back_24.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_round_bookmark_border_24.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_round_close_24.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_round_done_24.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_round_mic_none_24.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_round_person_24.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/ic_round_push_pin_24.xml (100%) create mode 100644 core/designsystem/src/main/res/drawable/outline_account_circle_24.xml create mode 100644 core/designsystem/src/main/res/drawable/outline_chat_24.xml create mode 100644 core/designsystem/src/main/res/drawable/outline_people_alt_24.xml rename app/src/main/res/drawable/round_more_vert_24.xml => core/designsystem/src/main/res/drawable/pin_off_outline_24.xml (52%) rename app/src/main/res/drawable/ic_trash_can_outline_24.xml => core/designsystem/src/main/res/drawable/pin_outline_24.xml (65%) create mode 100644 core/designsystem/src/main/res/drawable/round_attach_file_24.xml rename {app => core/designsystem}/src/main/res/drawable/round_cake_24.xml (100%) create mode 100644 core/designsystem/src/main/res/drawable/round_delete_outline_24.xml rename app/src/main/res/drawable/ic_round_done_all_24.xml => core/designsystem/src/main/res/drawable/round_done_all_24.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/round_file_download_24.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/round_install_mobile_24.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/round_qr_code_24.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/round_restart_alt_24.xml (100%) create mode 100644 core/designsystem/src/main/res/drawable/round_send_24.xml rename {app => core/designsystem}/src/main/res/drawable/round_sms_24.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/round_visibility_24.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/round_visibility_off_24.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/round_vpn_key_24.xml (100%) rename {app => core/designsystem}/src/main/res/drawable/test_captcha.webp (100%) rename {app => core/designsystem}/src/main/res/font/google_sans_bold.ttf (100%) rename {app => core/designsystem}/src/main/res/font/google_sans_bold_italic.ttf (100%) rename {app => core/designsystem}/src/main/res/font/google_sans_italic.ttf (100%) rename {app => core/designsystem}/src/main/res/font/google_sans_medium.ttf (100%) rename {app => core/designsystem}/src/main/res/font/google_sans_medium_italic.ttf (100%) rename {app => core/designsystem}/src/main/res/font/google_sans_regular.ttf (100%) rename {app => core/designsystem}/src/main/res/font/roboto_black.ttf (100%) rename {app => core/designsystem}/src/main/res/font/roboto_black_italic.ttf (100%) rename {app => core/designsystem}/src/main/res/font/roboto_bold.ttf (100%) rename {app => core/designsystem}/src/main/res/font/roboto_bold_italic.ttf (100%) rename {app => core/designsystem}/src/main/res/font/roboto_italic.ttf (100%) rename {app => core/designsystem}/src/main/res/font/roboto_light.ttf (100%) rename {app => core/designsystem}/src/main/res/font/roboto_light_italic.ttf (100%) rename {app => core/designsystem}/src/main/res/font/roboto_medium.ttf (100%) rename {app => core/designsystem}/src/main/res/font/roboto_medium_italic.ttf (100%) rename {app => core/designsystem}/src/main/res/font/roboto_regular.ttf (100%) rename {app => core/designsystem}/src/main/res/font/roboto_thin.ttf (100%) rename {app => core/designsystem}/src/main/res/font/roboto_thin_italic.ttf (100%) rename {app => core/designsystem}/src/main/res/mipmap-anydpi-v26/ic_launcher.xml (100%) rename {app => core/designsystem}/src/main/res/mipmap-hdpi/ic_launcher.png (100%) rename {app => core/designsystem}/src/main/res/mipmap-mdpi/ic_launcher.png (100%) rename {app => core/designsystem}/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename {app => core/designsystem}/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename {app => core/designsystem}/src/main/res/mipmap-xxxhdpi/ic_launcher.png (100%) create mode 100644 core/designsystem/src/main/res/values-ru/strings.xml rename app/src/main/res/values/arrays.xml => core/designsystem/src/main/res/values-uk/strings.xml (60%) rename {app => core/designsystem}/src/main/res/values/ic_launcher_background.xml (100%) rename {app => core/designsystem}/src/main/res/values/plurals.xml (100%) rename {app => core/designsystem}/src/main/res/values/strings.xml (75%) create mode 100644 core/designsystem/src/main/res/values/themes.xml create mode 100644 core/model/.gitignore create mode 100644 core/model/build.gradle.kts create mode 100644 core/model/src/main/AndroidManifest.xml create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/ApiEvent.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/BaseError.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/FriendsInfo.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/InteractionType.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/LongPollEvent.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/Extensions.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/PeerType.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/AttachmentType.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/LongPollUpdates.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkArtistData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkAttachmentData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkAttachmentItemData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkAudioData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkAudioMessageData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkAudioPlaylistData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkCallData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkChatData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkChatMemberData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkContactData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkConversationData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkCuratorData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkEventData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkFileData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkGiftData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkGraffitiData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkGroupCallData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkGroupData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkLinkData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkLongPollData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkMessageData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkMiniAppData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkPhotoData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkPinnedMessageData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkPodcastData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkPollData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkStickerData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkStoryData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkUserData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkVideoData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkWallData.kt rename app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWallReply.kt => core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkWallReplyData.kt (54%) create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/data/VkWidgetData.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkArtistDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkAttachment.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkAudioDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkAudioMessageDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkAudioPlaylistDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkCallDomain.kt rename app/src/main/kotlin/com/meloda/fast/api/model/VkChat.kt => core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkChatDomain.kt (79%) create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkChatMemberDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkContactDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkConversation.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkCuratorDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkEventDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkFileDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkGiftDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkGraffitiDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkGroupCallDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkGroupDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkLinkDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkMessage.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkMiniAppDomain.kt rename app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPhoto.kt => core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkPhotoDomain.kt (64%) create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkPodcastDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkPollDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkStickerDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkStoryDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkUser.kt rename app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVideo.kt => core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkVideoDomain.kt (63%) create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkWallDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkWallReplyDomain.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/domain/VkWidgetDomain.kt rename {app/src/main/kotlin/com/meloda/fast/api/network/account => core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests}/AccountRequests.kt (70%) rename {app/src/main/kotlin/com/meloda/fast/api/network/conversations => core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests}/ConversationsRequest.kt (67%) create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests/FriendsRequest.kt rename {app/src/main/kotlin/com/meloda/fast/api/network/longpoll => core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests}/LongPollRequests.kt (75%) rename {app/src/main/kotlin/com/meloda/fast/api/network/messages => core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests}/MessagesRequest.kt (62%) rename app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRequest.kt => core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests/OAuthRequest.kt (80%) create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests/PhotosRequests.kt rename {app/src/main/kotlin/com/meloda/fast/api/network/users => core/model/src/main/kotlin/com/meloda/app/fast/model/api/requests}/UsersRequest.kt (52%) create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/responses/AudiosResponses.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/responses/AuthResponse.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/responses/ConversationsResponse.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/responses/FilesResponses.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/responses/FriendsResponse.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/responses/MessagesResponse.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/responses/OAuthResponse.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/responses/PhotosResponses.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/api/responses/VideosResponses.kt rename app/src/main/kotlin/com/meloda/fast/model/AppAccount.kt => core/model/src/main/kotlin/com/meloda/app/fast/model/database/AccountEntity.kt (51%) create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/database/ConversationWithMessage.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/database/VkConversationEntity.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/database/VkGroupEntity.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/database/VkMessageEntity.kt create mode 100644 core/model/src/main/kotlin/com/meloda/app/fast/model/database/VkUserEntity.kt create mode 100644 core/network/.gitignore create mode 100644 core/network/build.gradle.kts create mode 100644 core/network/src/main/AndroidManifest.xml create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/ApiResponse.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/Extensions.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/JsonConverter.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/MoshiConverter.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthError.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthErrorDomain.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthResponse.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/OAuthResultCallFactory.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/ResponseConverterFactory.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/RestApiError.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/RestApiErrorDomain.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/ValidationType.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/VkErrorCodes.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/di/NetworkModule.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/account/AccountService.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/account/AccountUrls.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/audios/AudiosService.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/audios/AudiosUrls.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/auth/AuthService.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/auth/AuthUrls.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/conversations/ConversationsService.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/conversations/ConversationsUrls.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/files/FilesService.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/files/FilesUrls.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/friends/FriendsService.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/friends/FriendsUrls.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/longpoll/LongPollService.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/messages/MessagesService.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/messages/MessagesUrls.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/oauth/OAuthService.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/oauth/OAuthUrls.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/photos/PhotoUrls.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/photos/PhotosService.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/users/UsersService.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/users/UsersUrls.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/videos/VideosService.kt create mode 100644 core/network/src/main/kotlin/com/meloda/app/fast/network/service/videos/VideosUrls.kt create mode 100644 core/ui/.gitignore create mode 100644 core/ui/build.gradle.kts create mode 100644 core/ui/src/main/AndroidManifest.xml create mode 100644 core/ui/src/main/kotlin/com/meloda/app/fast/ui/ErrorView.kt create mode 100644 feature/auth/.gitignore create mode 100644 feature/auth/build.gradle.kts create mode 100644 feature/auth/captcha/.gitignore create mode 100644 feature/auth/captcha/build.gradle.kts create mode 100644 feature/auth/captcha/consumer-rules.pro create mode 100644 feature/auth/captcha/proguard-rules.pro create mode 100644 feature/auth/captcha/src/main/AndroidManifest.xml rename {app/src/main/kotlin/com/meloda/fast/screens/captcha/presentation => feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha}/CaptchaViewModel.kt (51%) create mode 100644 feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/di/CaptchaDI.kt create mode 100644 feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/model/CaptchaArguments.kt rename {app/src/main/kotlin/com/meloda/fast/screens => feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth}/captcha/model/CaptchaScreenState.kt (62%) create mode 100644 feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/model/CaptchaValidationResult.kt create mode 100644 feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/navigation/CaptchaRoute.kt create mode 100644 feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/presentation/CaptchaScreen.kt rename {app/src/main/kotlin/com/meloda/fast/screens => feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth}/captcha/validation/CaptchaValidator.kt (59%) create mode 100644 feature/auth/login/.gitignore create mode 100644 feature/auth/login/build.gradle.kts create mode 100644 feature/auth/login/consumer-rules.pro create mode 100644 feature/auth/login/proguard-rules.pro create mode 100644 feature/auth/login/src/main/AndroidManifest.xml create mode 100644 feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt create mode 100644 feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/OAuthUseCase.kt create mode 100644 feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/OAuthUseCaseImpl.kt create mode 100644 feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/di/LoginModule.kt create mode 100644 feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/AuthInfo.kt create mode 100644 feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/CaptchaArguments.kt create mode 100644 feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginArguments.kt create mode 100644 feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginError.kt rename {app/src/main/kotlin/com/meloda/fast/screens => feature/auth/login/src/main/kotlin/com/meloda/fast/auth}/login/model/LoginScreenState.kt (54%) create mode 100644 feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginTwoFaArguments.kt create mode 100644 feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginValidationResult.kt create mode 100644 feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/UserBannedArguments.kt create mode 100644 feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/navigation/LoginRoute.kt create mode 100644 feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LoginScreen.kt create mode 100644 feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LogoScreen.kt rename {app/src/main/kotlin/com/meloda/fast/screens => feature/auth/login/src/main/kotlin/com/meloda/fast/auth}/login/validation/LoginValidator.kt (71%) create mode 100644 feature/auth/src/main/AndroidManifest.xml create mode 100644 feature/auth/src/main/kotlin/com/meloda/app/fast/auth/AuthGraph.kt create mode 100644 feature/auth/src/main/kotlin/com/meloda/app/fast/auth/AuthModule.kt create mode 100644 feature/auth/twofa/.gitignore create mode 100644 feature/auth/twofa/build.gradle.kts create mode 100644 feature/auth/twofa/consumer-rules.pro create mode 100644 feature/auth/twofa/proguard-rules.pro create mode 100644 feature/auth/twofa/src/main/AndroidManifest.xml create mode 100644 feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/AuthUseCase.kt create mode 100644 feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/AuthUseCaseImpl.kt create mode 100644 feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/TwoFaViewModel.kt create mode 100644 feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/di/TwoFaModule.kt create mode 100644 feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaArguments.kt create mode 100644 feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaScreenState.kt create mode 100644 feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaUiAction.kt create mode 100644 feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/model/TwoFaValidationResult.kt rename {app/src/main/kotlin/com/meloda/fast/screens => feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth}/twofa/model/TwoFaValidationType.kt (55%) create mode 100644 feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/navigation/TwoFaRoute.kt rename app/src/main/kotlin/com/meloda/fast/screens/twofa/presentation/TwoFaFragment.kt => feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/presentation/TwoFaScreen.kt (51%) create mode 100644 feature/auth/twofa/src/main/kotlin/com/meloda/app/fast/auth/twofa/validation/TwoFaValidator.kt create mode 100644 feature/auth/userbanned/.gitignore create mode 100644 feature/auth/userbanned/build.gradle.kts create mode 100644 feature/auth/userbanned/src/main/AndroidManifest.xml create mode 100644 feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/model/UserBannedArguments.kt create mode 100644 feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/navigation/UserBannedRoute.kt create mode 100644 feature/auth/userbanned/src/main/kotlin/com/meloda/app/fast/userbanned/presentation/UserBannedScreen.kt create mode 100644 feature/chatmaterials/.gitignore create mode 100644 feature/chatmaterials/build.gradle.kts create mode 100644 feature/chatmaterials/src/main/AndroidManifest.xml create mode 100644 feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/ChatMaterialsScreenContent.kt create mode 100644 feature/chatmaterials/src/main/kotlin/com/meloda/app/fast/chatmaterials/navigation/ChatMaterialsRoute.kt create mode 100644 feature/conversations/.gitignore create mode 100644 feature/conversations/build.gradle.kts create mode 100644 feature/conversations/src/main/AndroidManifest.xml create mode 100644 feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/ConversationsViewModel.kt create mode 100644 feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/Dots.kt create mode 100644 feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/data/ConversationsUseCaseImpl.kt create mode 100644 feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/di/ConversationsModule.kt create mode 100644 feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/model/ActionState.kt create mode 100644 feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/model/ConversationOption.kt create mode 100644 feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/model/ConversationsScreenState.kt create mode 100644 feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/model/ConversationsShowOptions.kt create mode 100644 feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/model/UiConversation.kt create mode 100644 feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/navigation/ConversationRoute.kt create mode 100644 feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationItem.kt create mode 100644 feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationsList.kt create mode 100644 feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/presentation/ConversationsScreen.kt create mode 100644 feature/conversations/src/main/kotlin/com/meloda/app/fast/conversations/util/ConversationDomainMapper.kt create mode 100644 feature/friends/.gitignore create mode 100644 feature/friends/build.gradle.kts create mode 100644 feature/friends/consumer-rules.pro create mode 100644 feature/friends/proguard-rules.pro create mode 100644 feature/friends/src/main/AndroidManifest.xml create mode 100644 feature/friends/src/main/kotlin/com/meloda/app/fast/friends/FriendsViewModel.kt create mode 100644 feature/friends/src/main/kotlin/com/meloda/app/fast/friends/di/FriendsModule.kt create mode 100644 feature/friends/src/main/kotlin/com/meloda/app/fast/friends/domain/FriendsUseCaseImpl.kt create mode 100644 feature/friends/src/main/kotlin/com/meloda/app/fast/friends/model/FriendsScreenState.kt create mode 100644 feature/friends/src/main/kotlin/com/meloda/app/fast/friends/model/OnlineState.kt create mode 100644 feature/friends/src/main/kotlin/com/meloda/app/fast/friends/model/UiFriend.kt create mode 100644 feature/friends/src/main/kotlin/com/meloda/app/fast/friends/navigation/FriendsRoute.kt create mode 100644 feature/friends/src/main/kotlin/com/meloda/app/fast/friends/presentation/FriendItem.kt create mode 100644 feature/friends/src/main/kotlin/com/meloda/app/fast/friends/presentation/FriendsList.kt create mode 100644 feature/friends/src/main/kotlin/com/meloda/app/fast/friends/presentation/FriendsScreen.kt create mode 100644 feature/friends/src/main/kotlin/com/meloda/app/fast/friends/util/FriendMapper.kt create mode 100644 feature/languagepicker/.gitignore create mode 100644 feature/languagepicker/build.gradle.kts create mode 100644 feature/languagepicker/src/main/AndroidManifest.xml create mode 100644 feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/LanguagePickerViewModel.kt create mode 100644 feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/di/LanguagePickerModule.kt create mode 100644 feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/model/LanguagePickerScreenState.kt create mode 100644 feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/model/SelectableLanguage.kt create mode 100644 feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/navigation/LanguagePickerRoute.kt create mode 100644 feature/languagepicker/src/main/kotlin/com/meloda/app/fast/languagepicker/presentation/LanguagePickerScreen.kt create mode 100644 feature/messageshistory/.gitignore create mode 100644 feature/messageshistory/build.gradle.kts create mode 100644 feature/messageshistory/src/main/AndroidManifest.xml create mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/MessagesHistoryViewModel.kt create mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/di/MessagesHistoryModule.kt create mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/domain/MessagesUseCaseImpl.kt create mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/model/ActionMode.kt create mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/model/MessagesHistoryArguments.kt create mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/model/MessagesHistoryScreenState.kt create mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/model/MessagesHistoryValidationResult.kt create mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/model/UiMessage.kt create mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/navigation/MessagesHistoryRoute.kt create mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/IncomingMessageBubble.kt create mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/MessageBubble.kt create mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/MessagesHistoryScreen.kt create mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/MessagesList.kt create mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/OutgoingMessageBubble.kt create mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/util/MessageMapper.kt create mode 100644 feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/validation/MessagesHistoryValidator.kt create mode 100644 feature/photoviewer/.gitignore create mode 100644 feature/photoviewer/build.gradle.kts create mode 100644 feature/photoviewer/src/main/AndroidManifest.xml create mode 100644 feature/photoviewer/src/main/kotlin/com/meloda/app/fast/photoviewer/PhotoViewViewModel.kt create mode 100644 feature/photoviewer/src/main/kotlin/com/meloda/app/fast/photoviewer/di/PhotoViewDI.kt create mode 100644 feature/photoviewer/src/main/kotlin/com/meloda/app/fast/photoviewer/model/PhotoViewArguments.kt create mode 100644 feature/photoviewer/src/main/kotlin/com/meloda/app/fast/photoviewer/model/PhotoViewState.kt create mode 100644 feature/photoviewer/src/main/kotlin/com/meloda/app/fast/photoviewer/presentation/PhotoViewScreenContent.kt create mode 100644 feature/profile/.gitignore create mode 100644 feature/profile/build.gradle.kts create mode 100644 feature/profile/consumer-rules.pro create mode 100644 feature/profile/proguard-rules.pro create mode 100644 feature/profile/src/main/AndroidManifest.xml create mode 100644 feature/profile/src/main/kotlin/com/meloda/app/fast/profile/ProfileViewModel.kt create mode 100644 feature/profile/src/main/kotlin/com/meloda/app/fast/profile/di/ProfileModule.kt create mode 100644 feature/settings/.gitignore create mode 100644 feature/settings/build.gradle.kts create mode 100644 feature/settings/src/main/AndroidManifest.xml create mode 100644 feature/settings/src/main/kotlin/com/meloda/app/fast/settings/SettingsViewModel.kt create mode 100644 feature/settings/src/main/kotlin/com/meloda/app/fast/settings/di/SettingsModule.kt rename {app/src/main/kotlin/com/meloda/fast/screens => feature/settings/src/main/kotlin/com/meloda/app/fast}/settings/model/SettingsItem.kt (73%) rename {app/src/main/kotlin/com/meloda/fast/screens => feature/settings/src/main/kotlin/com/meloda/app/fast}/settings/model/SettingsListeners.kt (70%) create mode 100644 feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/SettingsScreenState.kt create mode 100644 feature/settings/src/main/kotlin/com/meloda/app/fast/settings/model/SettingsShowOptions.kt create mode 100644 feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/SettingsRoute.kt create mode 100644 feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/SettingsScreen.kt create mode 100644 feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/ListSettingsItem.kt rename {app/src/main/kotlin/com/meloda/fast/screens/settings => feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation}/items/SwitchSettingsItem.kt (53%) create mode 100644 feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/TextFieldSettingsItem.kt rename {app/src/main/kotlin/com/meloda/fast/screens/settings => feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation}/items/TitleSettingsItem.kt (67%) create mode 100644 feature/settings/src/main/kotlin/com/meloda/app/fast/settings/presentation/items/TitleSummarySettingsItem.kt create mode 100644 gradle/libs.versions.toml diff --git a/.github/workflows/android_dev.yml b/.github/workflows/android_dev.yml new file mode 100644 index 00000000..ac2cdedd --- /dev/null +++ b/.github/workflows/android_dev.yml @@ -0,0 +1,48 @@ +name: Android CI + +on: + pull_request: + branches: [ "dev" ] + push: + branches: [ "dev" ] + +env: + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + RELEASE_SIGN_KEY_ALIAS: ${{ secrets.RELEASE_SIGN_KEY_ALIAS }} + RELEASE_SIGN_KEY_PASSWORD: ${{ secrets.RELEASE_SIGN_KEY_PASSWORD }} + +jobs: + build_apk_aab: + runs-on: ubuntu-latest + name: Build dev artifacts + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build and sign debug APKs + run: ./gradlew assembleDebug + + - name: Build and sign release APKs + run: ./gradlew assembleRelease + + - name: Upload dev-debug APK + uses: actions/upload-artifact@v4 + with: + name: app-dev-debug.apk + path: app/build/outputs/apk/amethyst/debug/app-amethyst-debug.apk + + - name: Upload dev-release APK + uses: actions/upload-artifact@v4 + with: + name: app-dev-release.apk + path: app/build/outputs/apk/amethyst/release/app-amethyst-release.apk diff --git a/.github/workflows/android_master.yml b/.github/workflows/android_master.yml new file mode 100644 index 00000000..d32fee80 --- /dev/null +++ b/.github/workflows/android_master.yml @@ -0,0 +1,47 @@ +name: Android CI + +on: + push: + branches: [ "master" ] + +env: + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + RELEASE_SIGN_KEY_ALIAS: ${{ secrets.RELEASE_SIGN_KEY_ALIAS }} + RELEASE_SIGN_KEY_PASSWORD: ${{ secrets.RELEASE_SIGN_KEY_PASSWORD }} + +jobs: + build_apk_aab: + runs-on: ubuntu-latest + name: Build full artifacts + steps: + + - name: Checkout + uses: actions/checkout@v4 + + - name: set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build and sign debug APKs + run: ./gradlew assembleDebug + + - name: Build and sign release APKs + run: ./gradlew assembleRelease + + - name: Upload full-debug APK + uses: actions/upload-artifact@v4 + with: + name: app-full-debug.apk + path: app/build/outputs/apk/full/debug/app-full-debug.apk + + - name: Upload full-release APK + uses: actions/upload-artifact@v4 + with: + name: app-full-release.apk + path: app/build/outputs/apk/full/release/app-full-release.apk diff --git a/.gitignore b/.gitignore index de251274..7ccead02 100644 --- a/.gitignore +++ b/.gitignore @@ -8,9 +8,10 @@ /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store -/build +build/ /captures .externalNativeBuild .cxx local.properties -.idea \ No newline at end of file +.idea +/.kotlin diff --git a/README.md b/README.md index b1bdbdf1..bc048887 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,82 @@ +### Module Graph + +```mermaid +%%{ + init: { + 'theme': 'base', + 'themeVariables': {"primaryTextColor":"#fff","primaryColor":"#5a4f7c","primaryBorderColor":"#5a4f7c","lineColor":"#f5a623","tertiaryColor":"#40375c","fontSize":"12px"} + } +}%% + +graph LR + subgraph :core + :core:database["database"] + :core:model["model"] + :core:data["data"] + :core:ui["ui"] + :core:common["common"] + :core:designsystem["designsystem"] + :core:datastore["datastore"] + :core:network["network"] + end + subgraph :feature + :feature:chatmaterials["chatmaterials"] + :feature:messageshistory["messageshistory"] + :feature:settings["settings"] + :feature:languagepicker["languagepicker"] + :feature:userbanned["userbanned"] + :feature:auth["auth"] + :feature:conversations["conversations"] + :feature:photoviewer["photoviewer"] + end + :core:database --> :core:model + :feature:chatmaterials --> :core:data + :feature:chatmaterials --> :core:model + :feature:chatmaterials --> :core:ui + :feature:messageshistory --> :core:data + :feature:messageshistory --> :core:model + :feature:messageshistory --> :core:ui + :feature:settings --> :core:data + :feature:settings --> :core:model + :feature:settings --> :core:ui + :feature:languagepicker --> :core:data + :feature:languagepicker --> :core:model + :feature:languagepicker --> :core:ui + :feature:userbanned --> :core:data + :feature:userbanned --> :core:model + :feature:userbanned --> :core:ui + :app --> :feature:auth + :app --> :feature:chatmaterials + :app --> :feature:conversations + :app --> :feature:languagepicker + :app --> :feature:messageshistory + :app --> :feature:photoviewer + :app --> :feature:settings + :app --> :feature:userbanned + :app --> :core:common + :app --> :core:ui + :app --> :core:designsystem + :app --> :core:data + :app --> :core:model + :app --> :core:datastore + :core:data --> :core:common + :core:data --> :core:model + :core:data --> :core:network + :core:data --> :core:database + :core:network --> :core:common + :core:network --> :core:model + :feature:auth --> :core:data + :feature:auth --> :core:ui + :core:ui --> :core:designsystem + :core:ui --> :core:model + :feature:photoviewer --> :core:data + :feature:photoviewer --> :core:model + :feature:photoviewer --> :core:ui + :feature:conversations --> :core:data + :feature:conversations --> :core:model + :feature:conversations --> :core:ui + :core:datastore --> :core:common +``` # fast-messenger -Unofficial messenger for russian social network VKontakte +Unofficial messenger for russian social network VKontakte \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore index 42afabfd..3b1ff2da 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1,3 @@ -/build \ No newline at end of file +/build +/keystore/keystore.properties +/full diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 39128ba6..ce45158d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,226 +1,184 @@ -@file:Suppress("UnstableApiUsage") - -import com.android.build.gradle.internal.api.BaseVariantOutputImpl -import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties - -val sdkPackage: String = gradleLocalProperties(rootDir).getProperty("sdkPackage", "\"\"") -val sdkFingerprint: String = gradleLocalProperties(rootDir).getProperty("sdkFingerprint", "\"\"") - -val msAppCenterToken: String = - gradleLocalProperties(rootDir).getProperty("msAppCenterAppToken", "\"\"") -val otaSecretCode: String = gradleLocalProperties(rootDir).getProperty("otaSecretCode", "\"\"") - -val majorVersion = 1 -val minorVersion = 6 -val patchVersion = 4 +import java.util.Properties plugins { - id("com.android.application") - id("kotlin-android") - id("kotlin-kapt") - id("kotlin-parcelize") - id("org.jetbrains.kotlin.android") - id("com.google.devtools.ksp") + alias(libs.plugins.com.android.application) + alias(libs.plugins.org.jetbrains.kotlin.android) + alias(libs.plugins.org.jetbrains.kotlin.plugin.parcelize) + alias(libs.plugins.com.google.devtools.ksp) + alias(libs.plugins.kotlin.compose.compiler) + alias(libs.plugins.kotlin.serialization) } android { - namespace = "com.meloda.fast" - - compileSdk = 34 - - applicationVariants.all { - outputs.all { - (this as BaseVariantOutputImpl).outputFileName = - "${name}-${versionName}-${versionCode}.apk" - } - } + namespace = "com.meloda.app.fast" + compileSdk = Configs.compileSdk defaultConfig { - applicationId = "com.meloda.fast" - minSdk = 24 - targetSdk = 34 - versionCode = 1 - versionName = "alpha" + applicationId = "com.meloda.app.fast" + minSdk = Configs.minSdk + targetSdk = Configs.targetSdk + versionCode = Configs.appCode + versionName = Configs.appName - javaCompileOptions { - annotationProcessorOptions { -// arguments += mapOf("room.schemaLocation" to "$projectDir/schemas") + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + // TODO: 06/05/2024, Danil Nikolaev: придумать, как совместить с github actions +// applicationVariants.all { +// val variant = this +// variant.outputs +// .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } +// .forEach { output -> +// if (variant.buildType.name == "release") { +// val outputFileName = "fastvk-v${variant.versionName}-${variant.flavorName}.apk" +// output.outputFileName = outputFileName +// } +// } +// } + + signingConfigs { + create("release") { + val keystoreProperties = Properties() + val keystorePropertiesFile = file("keystore/keystore.properties") + + storeFile = file("keystore/keystore.jks") + + if (keystorePropertiesFile.exists()) { + keystorePropertiesFile.inputStream().let(keystoreProperties::load) + storePassword = keystoreProperties.getProperty("storePassword") + keyAlias = keystoreProperties.getProperty("keyAlias") + keyPassword = keystoreProperties.getProperty("keyPassword") + } else { + storePassword = System.getenv("KEYSTORE_PASSWORD") + keyAlias = System.getenv("RELEASE_SIGN_KEY_ALIAS") + keyPassword = System.getenv("RELEASE_SIGN_KEY_PASSWORD") } } + + create("debugSigning") { + initWith(getByName("release")) + } } buildTypes { - getByName("debug") { - buildConfigField("String", "sdkPackage", sdkPackage) - buildConfigField("String", "sdkFingerprint", sdkFingerprint) - - buildConfigField("String", "msAppCenterAppToken", msAppCenterToken) - - buildConfigField("String", "otaSecretCode", otaSecretCode) - - versionNameSuffix = "_${getVersionName()}" + named("debug") { + signingConfig = signingConfigs.getByName("debugSigning") + applicationIdSuffix = ".debug" } - getByName("release") { - isMinifyEnabled = false + named("release") { + signingConfig = signingConfigs.getByName("release") - buildConfigField("String", "sdkPackage", sdkPackage) - buildConfigField("String", "sdkFingerprint", sdkFingerprint) - - buildConfigField("String", "msAppCenterAppToken", msAppCenterToken) - - buildConfigField("String", "otaSecretCode", otaSecretCode) + isMinifyEnabled = true + isShrinkResources = true proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" ) } + + // TODO: 15/05/2024, Danil Nikolaev: add to other modules with build convention + register("staging") { + initWith(getByName("release")) + applicationIdSuffix = ".staging" + } } - val flavorDimension = "version" - + val flavorDimension = "variant" flavorDimensions += flavorDimension productFlavors { - create("dev") { - resourceConfigurations += listOf("en", "xxhdpi") - - dimension = flavorDimension - applicationIdSuffix = ".dev" - versionNameSuffix = "-dev" - } - create("full") { + register("amethyst") { dimension = flavorDimension + isDefault = true } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = Configs.java + targetCompatibility = Configs.java } kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() + jvmTarget = Configs.java.toString() freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xcontext-receivers") } buildFeatures { - viewBinding = true compose = true } composeOptions { - kotlinCompilerExtensionVersion = "1.4.5" useLiveLiterals = true } - packagingOptions { - jniLibs { - useLegacyPackaging = false - } - } + +// packaging { +// resources { +// excludes += "/META-INF/{AL2.0,LGPL2.1}" +// } +// } } -kapt { - correctErrorTypes = true -} - -fun getVersionName() = "$majorVersion.$minorVersion.$patchVersion" - -val currentTime get() = (System.currentTimeMillis() / 1000).toInt() - dependencies { + implementation(projects.feature.auth) + implementation(projects.feature.chatmaterials) + implementation(projects.feature.conversations) + implementation(projects.feature.languagepicker) + implementation(projects.feature.messageshistory) + implementation(projects.feature.photoviewer) + implementation(projects.feature.settings) + implementation(projects.feature.friends) + implementation(projects.feature.profile) + implementation(projects.core.common) + implementation(projects.core.ui) + implementation(projects.core.designsystem) + implementation(projects.core.data) + implementation(projects.core.model) + implementation(projects.core.datastore) - // DI zone - implementation("io.insert-koin:koin-android:3.4.0") - // end of DI zone + // Tests zone + testImplementation(libs.junit) + // end of Tests zone - implementation("com.github.skydoves:cloudy:0.1.2") + // Compose-Bom zone + implementation(platform(libs.compose.bom)) + implementation(libs.bundles.compose) + // end of Compose-Bom zone - implementation("io.coil-kt:coil-compose:2.3.0") - implementation("io.coil-kt:coil:2.3.0") + implementation(libs.accompanist.permissions) - implementation("com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2") - implementation("com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2") + // Coil for Compose + implementation(libs.coil.compose) - implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.21") + androidTestImplementation(libs.compose.ui.test.junit4) + debugImplementation(libs.compose.ui.test.manifest) + debugImplementation(libs.compose.ui.tooling) - implementation("androidx.core:core-ktx:1.10.1") + implementation(libs.koin.android) + implementation(libs.koin.androidx.compose) + implementation(libs.koin.androidx.compose.navigation) - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") + implementation(libs.coil) - implementation("androidx.core:core-splashscreen:1.0.1") + implementation(libs.core.ktx) - implementation("androidx.appcompat:appcompat:1.6.1") + implementation(libs.lifecycle.viewmodel.ktx) + implementation(libs.lifecycle.runtime.ktx) - implementation("androidx.activity:activity-ktx:1.7.2") + implementation(libs.preference.ktx) + implementation(libs.material) - implementation("androidx.fragment:fragment-ktx:1.6.1") + implementation(libs.haze) + implementation(libs.haze.materials) - implementation("androidx.preference:preference-ktx:1.2.0") + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.android) - implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") + implementation(libs.nanokt) + implementation(libs.nanokt.android) + implementation(libs.nanokt.jvm) - implementation("androidx.recyclerview:recyclerview:1.3.1") - - implementation("androidx.constraintlayout:constraintlayout:2.1.4") - - implementation("com.google.accompanist:accompanist-systemuicontroller:0.27.0") - - implementation("androidx.room:room-ktx:2.5.2") - implementation("androidx.room:room-runtime:2.5.2") - ksp("androidx.room:room-compiler:2.5.2") - - implementation("com.github.terrakok:cicerone:7.1") - - implementation("com.github.massoudss:waveformSeekBar:5.0.0") - - implementation("com.github.bumptech.glide:glide:4.15.1") - ksp("com.github.bumptech.glide:compiler:4.15.1") - - implementation("com.github.fondesa:kpermissions:3.4.0") - implementation("com.github.fondesa:kpermissions-coroutines:3.4.0") - - implementation("com.microsoft.appcenter:appcenter-analytics:5.0.1") - implementation("com.microsoft.appcenter:appcenter-crashes:5.0.1") - - implementation("com.squareup.retrofit2:retrofit:2.9.0") - implementation("com.squareup.retrofit2:converter-gson:2.9.0") - - implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11") - - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.7.1") - - implementation("com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.9") - - implementation("com.google.code.gson:gson:2.10.1") - - implementation("com.google.guava:guava:31.1-jre") - - implementation("com.google.android.material:material:1.9.0") - - implementation("com.github.chuckerteam.chucker:library:3.5.2") - - implementation("dev.chrisbanes.insetter:insetter:0.6.1") - - // Compose zone - implementation(platform("androidx.compose:compose-bom:2023.04.01")) - - implementation("androidx.compose.material3:material3:1.1.1") -// implementation("androidx.compose.material:material:1.4.3") - implementation("androidx.compose.ui:ui:1.4.3") - - implementation("androidx.compose.ui:ui-tooling-preview:1.4.3") - debugImplementation("androidx.compose.ui:ui-tooling:1.4.3") - - implementation("androidx.compose.material3:material3-window-size-class:1.1.1") - - implementation("androidx.activity:activity-compose:1.7.2") - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1") - implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1") - - implementation("androidx.compose.runtime:runtime-saveable:1.6.0-alpha02") - // end of Compose zone + implementation(libs.androidx.navigation.compose) + implementation(libs.kotlin.serialization) } diff --git a/app/keystore/keystore.jks b/app/keystore/keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..1d39b907c1e3fe9b131c676cac6f3e1bf3247ee1 GIT binary patch literal 2409 zcmY+Ec{~#iAIE2#`)1N2_wk!^bIdVp8AG`enYoWS@*`KumCP9mB~d))D0j-)EY&&~ zN@=Jha!-wxYdPZSd0x-+`@MdDd|%(!>;3tBzkj~rBz_{03j`UR z$RvJIFo|FAH|B+txWoSuaeIMD-1y(v_4hdQ2>$O%NPr7SCV_Y1BrpdK;}Q5j{{37A z4B=JJvKRK|ZXhq8?!#GeYVXt1Q9uyjxigpqrdOTKcVKL#Jg(mEN^(-WRdX6nbYV;#y809@HlyQ0eny4&Xyg34WJV*R(+>?k7K6AXKhH3e z1TD7fiOQ9mCN?H2E8>S^b9^qy+m1N zX^ZI`tGN)Rc;w8SNmFlh*iH|_$qlo?I0R%SBQ8nC&JR{g^k$Om_1;q^twfO$$t-ChErYT*gX>>mAsI0Ho<+ZGil0 zo0=7Vk9(?+=`y8soELq8pz*r2@e~(JW&oxgDkO{Q0SE+*k2+Q~(AMv$g>hN<`=#uQ z?pHUQz>EMUrv|yym<39Xvpnd%u_!=LGa9`ek;t+Z@v+{2K&T{29FmR1q!gx^od7qJ z%VuBJzXM>^>~#mfZTC&U= z!n)!Xw)Zl8=Am1!997(R&8e2leDu=bh{-s!AimJI{%K=Exy-5Ff?2hKT|;|1_j0hukdp1DeH-rfDU)>YrJ&EO|v)-6ZY(O@~1BD(ri(ud6UIsnz25Ig1`iA#BeX`6h;m>?*%kVKNd5Tj%FAEN92olw zHt?Ft92x1G@BYJMjsOZNjW-I_Rq<|fZ}%QMo4_o}N~l@cA$7&nwSDOs1!6!_)lK^S z`hGjZA&irQFk8-_6D5&cJY@|{W{9w=US8#nmHw%(Ob)1WC6xq@othhEP!?b zSJ~*$fk82z9ZzP4AKf9h0M_pXntMlwGZdZF1bVr#$8+dS!$){!ac4dY}WK4hQ!uVXKf zK$!QNKN+(E=vn=;f>jx55vnz6096a)mSAn)&Lf&B#L#qew%n8vVk(`!-zX^!bkE{Pe*Z>=oKii~FzPrn73UmH2X!M;I{|8@VT>Q7O zA!3d}d=9soFu%8~RvKlz-Dd8B1m-NHX6t;La=56Q{2y!LBI^}~I>Gh_cZ|32C>5^L!-3h~psjlSl)c_vKi`MeRK`h}t$exkk3>i`_d~Y` zrnX&*5x11|J^N&kg&9Y7@ZH-EqT`4`4QA=Ow@z2N@6|&iyZ}$n|YGLAw zM>rD&@@(^>xRsRh>DtL<<}Fs`;bEY7ap75CO);`*JAF0#rd^%?Y7W|QW&Qw{Ykvhu z)q`AICFTnCbu$Ww_FJV|S=ZNMHU;tto3g!IxhcZiOH1+}cms^d~V)_Awxs2H>X0J@do{bH)uK4smd%$-bbxXQQJ2ZzVX^z z6F-mBH~h8)vl>B{#oL?`YVLYZ)e!7k$2sK7kr@f$$^%gi5T(PoeR z&duAVnn&7i)W6AcGkYtofV=%w(1kK~d{qPTLO5KLmo_*XzOEekOVhNFZgbtE; z1-F1J!uff)HAO(cBisNmMBS&DmK5Us8X=>7yi;Ke^3ZOH0){Axi7rejmbRPrdd^i< Qt<-(a9asW_xU$Iq24yN(L;wH) literal 0 HcmV?d00001 diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index ff59496d..2f9dc5a4 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -18,4 +18,4 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile diff --git a/app/schemas/com.meloda.fast.database.AccountsDatabase/2.json b/app/schemas/com.meloda.fast.database.AccountsDatabase/2.json new file mode 100644 index 00000000..116a04f5 --- /dev/null +++ b/app/schemas/com.meloda.fast.database.AccountsDatabase/2.json @@ -0,0 +1,52 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "3ebd234270e36902d3d461af38664869", + "entities": [ + { + "tableName": "accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `fastToken` TEXT, `trustedHash` TEXT, PRIMARY KEY(`userId`))", + "fields": [ + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fastToken", + "columnName": "fastToken", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "trustedHash", + "columnName": "trustedHash", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "userId" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3ebd234270e36902d3d461af38664869')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.meloda.fast.database.CacheDatabase/3.json b/app/schemas/com.meloda.fast.database.CacheDatabase/3.json new file mode 100644 index 00000000..0e8d3d0f --- /dev/null +++ b/app/schemas/com.meloda.fast.database.CacheDatabase/3.json @@ -0,0 +1,424 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "6a940f719e8dd56ea5c196c152f1e536", + "entities": [ + { + "tableName": "users", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT NOT NULL, `isOnline` INTEGER NOT NULL, `isOnlineMobile` INTEGER NOT NULL, `onlineAppId` INTEGER, `lastSeen` INTEGER, `lastSeenStatus` TEXT, `birthday` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "firstName", + "columnName": "firstName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastName", + "columnName": "lastName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isOnline", + "columnName": "isOnline", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isOnlineMobile", + "columnName": "isOnlineMobile", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "onlineAppId", + "columnName": "onlineAppId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastSeen", + "columnName": "lastSeen", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastSeenStatus", + "columnName": "lastSeenStatus", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "birthday", + "columnName": "birthday", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "photo50", + "columnName": "photo50", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "photo100", + "columnName": "photo100", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "photo200", + "columnName": "photo200", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `screenName` TEXT NOT NULL, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `membersCount` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "screenName", + "columnName": "screenName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "photo50", + "columnName": "photo50", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "photo100", + "columnName": "photo100", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "photo200", + "columnName": "photo200", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "membersCount", + "columnName": "membersCount", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `text` TEXT, `isOut` INTEGER NOT NULL, `peerId` INTEGER NOT NULL, `fromId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `randomId` INTEGER NOT NULL, `action` TEXT, `actionMemberId` INTEGER, `actionText` TEXT, `actionConversationMessageId` INTEGER, `actionMessage` TEXT, `updateTime` INTEGER, `important` INTEGER NOT NULL, `forwardIds` TEXT, `attachments` TEXT, `replyMessageId` INTEGER, `geoType` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isOut", + "columnName": "isOut", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "peerId", + "columnName": "peerId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fromId", + "columnName": "fromId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "randomId", + "columnName": "randomId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "action", + "columnName": "action", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "actionMemberId", + "columnName": "actionMemberId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "actionText", + "columnName": "actionText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "actionConversationMessageId", + "columnName": "actionConversationMessageId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "actionMessage", + "columnName": "actionMessage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "updateTime", + "columnName": "updateTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "important", + "columnName": "important", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "forwardIds", + "columnName": "forwardIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "replyMessageId", + "columnName": "replyMessageId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "geoType", + "columnName": "geoType", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "conversations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `localId` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `isPhantom` INTEGER NOT NULL, `lastConversationMessageId` INTEGER NOT NULL, `inReadCmId` INTEGER NOT NULL, `outReadCmId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `lastMessageId` INTEGER, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `canChangeInfo` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessageId` INTEGER, `peerType` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localId", + "columnName": "localId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "photo50", + "columnName": "photo50", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "photo100", + "columnName": "photo100", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "photo200", + "columnName": "photo200", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isPhantom", + "columnName": "isPhantom", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastConversationMessageId", + "columnName": "lastConversationMessageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReadCmId", + "columnName": "inReadCmId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "outReadCmId", + "columnName": "outReadCmId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inRead", + "columnName": "inRead", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "outRead", + "columnName": "outRead", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastMessageId", + "columnName": "lastMessageId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unreadCount", + "columnName": "unreadCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "membersCount", + "columnName": "membersCount", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "canChangePin", + "columnName": "canChangePin", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "canChangeInfo", + "columnName": "canChangeInfo", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "majorId", + "columnName": "majorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minorId", + "columnName": "minorId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinnedMessageId", + "columnName": "pinnedMessageId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "peerType", + "columnName": "peerType", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6a940f719e8dd56ea5c196c152f1e536')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/meloda/fast/tests/LoginSignInTest.kt b/app/src/androidTest/kotlin/com/meloda/fast/tests/LoginSignInTest.kt new file mode 100644 index 00000000..f436216c --- /dev/null +++ b/app/src/androidTest/kotlin/com/meloda/fast/tests/LoginSignInTest.kt @@ -0,0 +1,46 @@ +package com.meloda.app.fast.tests + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import org.junit.Rule +import org.junit.Test + +class LoginSignInTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun signInButtonIsClickable() { + composeTestRule.setContent { +// LogoScreen(onAction = {}) + } + + composeTestRule.onNodeWithTag(testTag = "Sign in button").assertHasClickAction() + } + + @Test + fun signInButtonTriggersSignInAction() { + var signInClicked = true + + composeTestRule.setContent { +// com.meloda.fast.auth.login.presentation.LogoScreen( +// onAction = { action -> +// when (action) { +// UiAction.NextClicked -> { +// signInClicked = true +// } +// +// else -> Unit +// } +// } +// ) + } + + composeTestRule.onNodeWithTag("Sign in button").performClick() + + assert(signInClicked) + } +} diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher.png b/app/src/debug/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..5882fe4dcbcdd66282d3f7d59fcedf3c633770e2 GIT binary patch literal 3275 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 JPDHLkV1lXjJ#+v7 literal 0 HcmV?d00001 diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher.png b/app/src/debug/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..ccde9097cd418ff23b1a4432a4a3e303954f0695 GIT binary patch literal 1980 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 Ttgge#00000NkvXXu0mjf^keK= literal 0 HcmV?d00001 diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..2595694270aeeb7f5d55794cfc825f566108b4e0 GIT binary patch literal 7262 zcmV-k9HHZhP)aW%X>sm#y7Oi3h z7qk^ED!)QyQIS;y1y@uMltmT;ftl>V+w&3_w9nwh{s#xj&#e&ZsuC7iB)Vb4}eN}nKPqXU-UYDiOS5Xe&HQvMj@ELse zhe*;%tQeuxBb^qZ2g3NL+a@;o9Iil}^HTp**Gxi&1p%LHWuVTrm0)*Zs_O_jHWRcH zoFL#R@EY$m)6eYY&*9H?FA%QfycE~QJ*4-Abh^M8nUp0xkp|y<-jYmpT}BlAH-DXL z15x@x5h*(1?+2vU!ad0Q>l=NpM{#eo0c|;{Z80(pzA*PBevfxUQ(dPM%P^Z9H}e)l zq}(pWJ5L<b(|-9AQr6FX487n1>! z?yLkmwUpo6qza;GMnDkSm#5sPd zqnfyaXNUp<5=+Co5xD{t2I@W~t}cK+p>Li2juEMUB6U@Psm`e+(^)QYQPk3OqbN0i zSthYn;```6wWo22$qxws%@f52O?9qH^6h+GMjS=Br*MJq%gIC{eJ1Z$b&d;y?O1%& zyNHR??5WUFSH%WZh;3wpA9dhkB@vOH!B})RMlqoxwmNuCO#2OF@Q#Trj(XixUvY8< zcMM~~EKtlEiA3z*VX|rcCQ4mZsF1og=a!S)+E9;;IJo0)oT4_L=E^67vifAJ)6F2p0Eh$a)fhW|Jcm*NQe3ug@H)>Y1JTfF zb%wON3KmX2nV@6VQ15YG(B0TZrJP*XaorcQI{iYYHEO7H{aM2T>}ZXPI*Jkxm`vgJoZgJP`m-qI$36yN_#Z7O^f=Bb{!`XUhxJIrqiL z6LIh)I^?nM-&|!>mpfx=RQ{Ea*sau8c+bq_JmH@^ES`5;om) zn~X40G~IEN>^x<|$uCel=&^I#D$N&(bvEe2*!-=>swIuA=YwXIE&vPs_r)@@bcdz~ z?v$OYc+drEd#02hS?SUji*+|>a#Gym6P9GCc4CxdTg{LCMMjqH&@}6A`S~gUbaRx% zLp!Q%>xs^I@(gM`L~9Gfj(MGTs5$Ndnr^>AMwFhQRssRg(Ls*`uIzs3ZqVvOENK4D zYj>+X?g{+-JQ+!PgPIA%;vwS7xHT0<4|Moeq)8+)d&5biHmcfnO`ps)KRQQ7lHQd73Sxl(*aNGS>XKbWbE=K?BJ!=z`61Nek&tJ zu!t4{v4DE_WZQ{lR=o$h9ngT9nm7c`FRR)5_%A$DMv71nJp}5S<{)YPGNN04lTfMN z!y?T3?5(wXY2d6%%X9qHO0M~@eREyWNFV^Z+%?(8y-}_|13DfME5xkNu{!7f3xTsb zrz;w-|BZ|gp(5G{1YN=kyb92oP{aH?i5Yh7d0_%&}08S z&Ae1+ef(#i$~Dh_Oh$%K(elb7u5InxQAwaEPs8!()=OOc>;9buQ@Un`+Qwvuc6k5 zQ(N%dc+C|uGV}$lEC0=tCz=Vwg6XeFFumAZG7Y-#S1?UY*eQoS&RJ@7U;dx?xaKGS zCL=?rV6H3r3B+gE9a#Fh+F^VQy6>|aWA5t_!^APT3-5xj66euKnPW^R%oT8bS^j$GJ!v2dT0?VVY~E|5yR?X0?3kLe zFFf=oB|~4(o$DGZ1Y+6Pr_~n!9D~*hE`(H6WZBoQ$+ioH_(vtda|n;-^oE{X*H9r4 zEh?VaJ-M8F*;;%RxUk4zh?DZ9N1;A%;X)!f5mX$+;-6W?nW+tR&TshmN9isMkLLXCAsHD$MazrN$;i?jh6aIz$<+_4 zO#3g%O34N%f?6j;P1PwID$M1?`tX|+DLsI0y!8(l#V~Iz|KO&ugsh zkLtCx;Kbv>RHbPEJz{&6?L0n!_9=Vms{fL+WMt?IT3>rvMwFf~LnZjdh4{yzT=&(FM{}g;Ep3-#s1OL*_x@Ttx5TEu7n}%+K=ecL-`3h6 zQF;K~c;mG)BJ>q)@2-;(r6&vp0s(B5;)?RpXTXV|2sG29K@605*z0&z$prFW)W1h_ zr0CeUhim`lYp(sXPks<>>sIq6gKz^=Fw%YwDv0inc%9rk&N3{vA4R#*yLdGGhep+V z9q%H5R-_A?TG8)EbEIhhW;b`@#EHR0M+eusbP35Sr^x#}2!QV4%HDMww-QbyQEv2{ z%FopBY1#-~iYB?i>-NOX@d2DP9W>FN34Mz3y+5@_U{;@kpa zy1r8!f$E6snsc8_`Qd=l2-Ldj4SD|~4N9Pot1L%;m6L7+H-aHh+yYavo?O7Mv?5~T z?|vy$e7K-E0=>CH*8fO@6%#+Lwgzr2%(ZKRCD1}sfr*f(SxE%)pM4tFyrBP&=JW)m z5vXPHKV@SOX^5)PEw}2jD#49j2}DcP@y{8Gtb{zRN+M9x?0aNN4<{5wAd-y*uDDP( z29X9M(DrI;`>ln!)!@d=-fBUNKu?$HY)T?fZ$CW{E+~mWEiXPV8-u}s$O5$mMIdUP z;-4_)oBcjllTrxObX)LS@b(o-ArK0h$EVfE#$YheWq~Zsf5_uqSo~FXVN=d-g%Rjs zLv9(lz;UHyVW`qd6&`LVg+O?QU)uk{!ZDhKeNtudUt2KA32p>Mpj!&EiwOG;D}_Lf z*9~0u#h?TNwJ!OWtnY&gN}!Lb%=MRMrQz2PdnXWEiFkQtO3o3lau$>M{^|Ne+f z>EXm+6R6|RK?Tl*2?W~WHtnC5np6UAc!cSU?u8MEU_!mm@u`xgG6FxJ5thf4d%|E7 zsPT{gE9>)MgH2^@s4(szoX7w-C`Wpf;aXu=hLl4d`vxTu2tX~U@uuszrdf9hn?;2Y z?K{7eDK%2S#tqWGDgs3J*sWuYv+M)HiHVdOJFJsxe&$kb@yzIv?zF&RE%;>0oJnEGk8dCpMD z#3Dr?)SRUERvK6ahAn@eH|ges3zQSRrtnOLpD6@ODNAjomkpIbIuZnG|7ff1zDfiO zpuecGat{<~e+w>9P6S1uVbgW|bjH|g@-l4=K4-mB=EMdP15TjURxa?fvt{>HBoBMv|IBE6>)!3v1qOeyCU7x3RhEt$3NkJM2RtDJ&`gHu zgk>G$2(%=f-xlpwXrKs$r6SUMDH=q_*qQ7Nd+Bzf`yz0Fav^B<1;K|$)QlZFo*c8@ zt2E7y5h!%D@7~Ej$1lB?QXzI<^UDgJgzhVo`Q7S!eMVR2B}tm3@s4Rub%Jf$%Z z2s?q`x_)(;>^@2cb6;C4&23lbq+bBt_a**e0-qO87&L(#wO>uKDQiW5!@_n7s9FVG?Al5P zoBR42!Lw2K0(5O2GbRnXryS_B@YxtPoFd1}>$Mrv4tVXY(KtUA4gz%?_?CYJNqTLi zg9XnU-Nv?GWTc$~T^EekjD_wg2l^#2VTt*pN2ilx!Ild1tI=qM!a*Qx`YFAx(jlIC z|EH-4i{}fduCX$;U+#+uA50sBU^@Gj!kn`XcpUA~meTi^K>Xf@-CS4s5UkIp3RB0g zveQs`&en$^coqvT0w#{i8#M|Km+98KjEk{*XEa%%{t&3+@IlW1Q=jabN(Zw(SUA7X zsQWi`OLfc^kA;Om5Cl?j*~uDMq1<0*Pn!PCl=8-CvO@hKP}A)<$gZaj2-atv%h+^L zM%ua1ZB`Or`@t42{R*C?!mQBf;>5%(a@4JG=;t5yI-2!&S_)!&1qK74TmI|NJQ-Og=nH|G?z~xc9W{Ve zoj1TXVV{Jmi)I7xz3V}NO$2tG`KayQT z6<|{qZbS3=X(Pf&*)CiKb2?<(qELmvM--|N1nkQOAAyCttdu8XV0c^d` zwn{TMyF~jCbVqfFnFi=|K&=lGW`+2DgvKUl^2xF6O=sy!?Bt-|8ZK zJm2-4rDzRwMs%*iopiUjd?_5=AN=mGJztX%H>q*o|BBbL|vdGZX#Q87& zsq9>(gOTSu600t=m+mhelQ0E3Bf86k4yi69Mjj?CpdQOy*)0V*mv67O@IMixws@#z z;e0t^B*3pDO3zaT=yP5h+{P1E=Vks6bd{Ho0PBMZ(*X~hdlKf#FzGfSJ3d}Vj%81o ziypw1Ai{60s;5frzu?CtIQGj(!_fZmHq|_uBLj>Zsvuwo%Ht`{uD9}->Qrquty zmb2^&{B(4TM3Dg_&)(W{?qAlDodrpWHP8js2~ze!k9&F&77wAKnMwn-Qg`Y&@7vC@ zub3s0b~uLcK}?=kZ2GTl6UI)34u~!gR$!tOx=}>j)03cKsAwWUo5(UVkZ6Uy^m8@F zL}~$xd3H}O=N8*ac2M%Lj0+D%Kqp~aWjK(+raJh?fhhaS3X>A6Un|#d!TJ+{MCwWL zl@6rp7*jmc^}M-gTYgdk_VfXHF!so}po@r2RfIzri4g6lXT`@ClcQne$dTp5FTc6H z+RE37FfS8}i~0^$utlBc<0=dHl&R<)GR6*!H63#-j)x8yc@%Gbn6S)@PwxpPr3xJ? zBOSCpYtp@lwJ5@quC9(4i9!c+Sp?jc3KREOS;5O>Y;720I@VY%Ax_s-Lmnnl5~b53 zRw?PI<>9h|2k|7pL60LA7ljB$9<0M!<2H5NQ=IoO#uQ^q$Cz0j)slyam_+cy6B9I= z{FJyj19@%!O~plXd3axs_IJ47z)1Y*f0HvQf!b0+;7aAsUJfBq895pe7tJckzyBSVF@WkRfhcTpMi8004MvdF*N!YXp zR>5H@B?3Nex?qMuB-@?ExwpUJH0(jT&oVXCjfMh|ggy#&9b{Oa*I2ow<)!;>Da`p3 z&9Y2%th99OFoqFdQ8?JbNW_9`nBQ#jjVc)($CJpClaw&|@v?$v-*Fj^@5Is&A9m0p z@lljoB$35w$Ko#}7H^Zj|5dMjE}O(U7!!<5Qk+JQv7%$fg5@Da9wy?r2<8{F#Hd|} zt&w$l+Jv*_8w!@JbD3I@d0?=4i$r52HIu2c{lYKfbbvUB0oS^WE%S_pubrAU;p}d# z4#o&$g)xijcqdq4)`=3B(q>pf1EMloJWakd&6G37uDb~(Q3wh*jNL{YYN?<=XJ@ukF`uxWi+e)^r zcN?0Ky}>d;ktY+%aY9V0{>D(^Yw*{E1*Gu|BMht~EDcDrvBKDlHqI{AK7LMm>cwa~ z`hdQmPt@XQX404R0^tS0enzS!IMk^;XU?$pzB;*i(MCF0$~vFe^_PaRyhp^ zU$B&{MVro_H1X%8jZWHD3)+r8&=`kV97Y_Z<}u=gLBvGDpki=g*0Ih7fn@Aac84rjBx5DrHNG(=xfJMP) z@c<>x#qM6PE{MlAxQxe^*-LjkPws_#UaifzR-c@FI<+)tlND`5ThV5;o%R8JL7!q4 zX8=V^B*X(K+oOnv%mgMMb3uiSOd`wFNhpE}PujS%uFlWAW@b_DpC2tPe0qVg@byLJ z;tfm7^`9)am+pGYZrHolX*{&SWvGAGZ92M%V58euPg3hctDVNZ%N?a(EhC?OiJbE^ zITzQsqd50IToczGF=9j|y%$dyW@$vEDWq*ho6&Xz$LI_C6eCUqgb@h=B{C9bm7oYl zCd>`xldp0K2}+Gn-uSM#NlGdJ7QzYTXN^hlmLw-nvyV^yaYgF*GXcEDd-xwEh>O4> zI=_=FxCib?Vs!nFe3rK=L^i{KP%We}+1;W}6Z1g=mvVv#~99~J{dpeUo@|Hl6)LEwgP4$j3ja4lRD*Ty|? zFWeLNMjK+JQIUwr1lIti!A&4IMIjC~6<7kqPmEZw5O6I}J_1@;4Uj<$yodkcGx#jd zVMM{Ta7|nr_rSeyPuyGVo?@g?p@<13CKV$VECUn{D*@#Luo~Tg{U4vjIXIUQMNF0$ sVWM1^a;4-F17r|G`hPJ=hAdhB1F-538Mg;nC7BpctO+wQEMIM-jY7lwbTGwAh~C#-Te*vmm;HDt9o4TXJ|J4#{x|*D)kEZ# zW@)?Og`W$f?DqYa#j0ECrSd<=`&go+2Tb-XJFK%hZZNq5`0)+RBVm4*-Dxg#UF1*0 zpX2l021HN0o6h39xAeAWx(c^Ji_Mn~%)4B^1}I|-*~J+Hhcl5D?MsmsyE__bTx5JD z0-LKz7bL8J872Vc>?{WyHE6Xq*#QT}_n`T;$DFFz1Et(zo@33F&FHmxdFJv&NSb11 zrF;C{fbH>RA8&wy5bcRb>s2ZOigs_{c1TDtaI8KTQd|VlRaJ2*&$bsbFq?r}E5KT8ubz9Zli$6~MaC+RECP#Issn zdF@c2=+H-0iY3pG-W2l(sSCBR=*1w49iz$|K^8{K14_zk6{>l4(<4s;_QzyLjagBH10a!y#)lNnO_q&YCs73-`QMdxn})-eK(&?J6YoI%L;2s zG__1y#nwQJtY!4ipyyn>MEpxj8y z%RivvJ5zka8+oLQHu6#KS;!l2AtqiGy6Z_<27C;}Mz8D3T6V?H4_@AX{v^h!!Xh@Y z$h?#wm-E4`D!G^+a9xN+ZK_rdpm`e9HdX@b=k&6m9ryS*(ymHN*Y&sz_{^>zoprDQdFy>` zIyaXMge$>5&i{ih*SLL_rv)}+g}gA*l{GXhA!^!Irn>ztax{Rm9VI`?OFZwW8hTzL zLZJGi&AH|BeMLy)jPbL9450!cUq9hx#zT1`$)8K++x;t(voV{_Lt3_He}A^)(_@x3 z;}30~dIrfRtdYz>zVdmCHH*4rt$W2D4s#3>e3lw+Y3iEVxo&OJKlAp;RHZrb{r%58 zZ0U03kmkDqEnlOgPO9G7gvi!Cbq+_$t256tM^b6#6Nv{@WlcVJqwJN-jv=@Ld55oiLt9zI_y$}&Yxq6;8Y7)2=naNs?&XAEAoY#oOkeZ z7F3>!PUxqQ*i;LKr*G!RCqZ1l!FR+!78F5MQHVjek#=AJh#8Ks*rQ#LsjM*!A^l7d zEQQeVdpzd4g;Jf|B~($ERJLl`Kiq36EaY^?%D$^?F+IuMFbps%mxC-GK(XKL(b7yC zyw%vC9m}aP*#2hXc-EnEt?I|#i3$-8Z^S$%o~LiOh>zUHejFSz4p1$JFyCBH#xzuA zQhfW{Qb?6b_x*YAHdOk%T~mM1QQ3!aOc<(m_s8)EveoWNjLegVwAxH8)#3(4lb0dN z8SB&vevj|CZhxPjrjvd3rPb%DAHI)_IRHLKnXj`?l~sLJj}q>?Z<>(|uW~%}4(7o* z>aF@ap1ZAo77Kc99C9`0@xU;pBnvj-@05e$4=b2x#}~Z0shI!BB~E&M3SdEvNMyz) z1q!eQHSMiI0V^*#%CQ2)>mM&orl9qB%rKW;v@hR`UQ}JpI(FqYlL2w9jr^soKE+oi zULIT@OMY&a@pLsNM!MPOO6v#k#iQ+xEwR$yG&Ua8MGkE}LsKGhm9J?Yl0RFCc;WAV zb+VU=1oNTJG09p&gXz%$a^QtMy6)|%D^)lXRsMR<&LBlZ(w|4K_WOqpVV&2+l5chl z0lf4#?jOQW0!yG0&CiAwgsi$}K~2M#dAw&tvbD<=mq~u`WWy(Gy)*13Aa5mu8B?QQ zCHX)CoaYc#trb4e{YjfO^n`yR%DxWAS8{kA-I;T$>DEK-j5 zmjds8UHLK;!!zK*&n5Br;$K*!>QJA&p7lkgLM7xO43c2dqCKv(4dRxRk$np8Hc zDQ`W(aOFT-n2nP!!x{{)d{-$YKd{+W*mak8t(`I~PM0Do5M`>^&gaMa$t|+0paOfZ z!xatC`my8SyA1Id8hC?9O$wRiZD8o^{V((Jru~1!Cazyfm_35J(A|ifTvq>a?^||@ z)bzwh!?Nxk6}?5obte2*7d&pmxk(7YrT`B$;3=X0KscGpx$o*?0fK0Ptz41Z$c8Ps z`{%glabi5LGvTmdvJc}bXa_$I^yc}ULy7aS$nUbEu6B7dQLP8Ti$~#$<@=_`6$?W! zJ!dl@jVRHILKO1ME&CC0#P{M0X?j~+OwDpo%be7Yma&Emo50ENX&GduwsEo;?JDRs z8WmF<$g$knVp+)7Lr7el^DWc#z3YtcabkOWfQu_FV>19GPmNB5%M1Nn3BWAMBerZu z4J|IN-BT|e@6TPk=3d&7x%Y%mzeD|KWr*w*dBXO?$x%l$-7>f|0p!K*vLl*n7l@?D zD`9qiBOmf%Q~D)1D!QPZcW&|0FIvSIH1+htUgbo_`hbm5-Cn4^A4R#j_W@asc~ zvci38V(IZ96DViFe+2xJj_%uzGezK@3;8fC> ze;XwJ3~ReOz{4!Q{ao3fNjo!ETP5nwq;r00wN14un*rrJVNi_EA(7F><<)UbKmmUb zIC^-|pzvnx1&TB$&W9HM2}Nj@aj>N_!(MhhlMP77B|(m{KgivmBce8*&`ZIkb2#c7 z2d>Vg*=|+b&L7om!%6{x4L?Vq#JGWPem$RNeOXtkNN+Bu&DK3UEI%m zkl=l(#1La4$D5esNUCn?x{RO?wLrCeFIA(__Tu#|ibR?Mx`NSrj{SKDvBgs1V^O?` zkapBe0~tYKtL+kcr|J}zv*+>aCt>Y_0_6qHJb_?LHzJLa&eFzv`a1II;38u8PAU0dso53X=K<48KxNnKWr4h?~1C(y*9->lxi}?cO zgQl&XK?y&|=Z`wHl;_>P28`)*uX_T+up~H6d>e+Zt6Si>l@U|;g{c;*PGz3RjjWE$-kxTM#W9U33tO*?>dsvy=msMHqQgq;m}WT zW1p({^#QvzhSPj?8fQOOo!%@mZaq3>={-8`Jx0l7)&fiv2N?u( zpEL{GpQy1ci#d7a1`GrE1hAziW4FG#}4jA7dv} z9li8@U?ZDD3&v%90sA7Zd<0e94Or!EmMkDEbXue7Ix)kqWNTFmic$TtSi$VvJle5( zQb|Z5wl`6&BEvU(SPfd~RPp zB_QAvMFTN^JNCj)25}E&+mRT7x^EM z9Z2vACjDTB?1s&=&>o7HcwaHQhfo%qwFarNcl8p$E!5#l?(qmxx~le4=8x4+5fpPL zY?dQk6qVbTW7W0x2<}#-|2s^l9Z2xb!s^&}DS1msPJDhN_?O{-(P25zO!)xPg+9ul zmd!YXC2PaiN=e(n!h`D=s(=%g4Tx~f(Q_}^d9*)SEmxy=?jF4zyXUsV#C_Ua^1||% zN!TXV0^I|nYY!yw&G7J^KZxRCXu<*;%HrKoA__TSXdMr34P0dP`f2f<*;{ZGghj_o z7Q;p?FdNGNisZ^vB!%@bVi2%2O-tran?9vpXOGO7ie$N&Z}Hua5BK3N`8qqxb&@ZT zXLX}3S4BJTr+!&x2iWBoSJsIG$>gpDpag4Lk-LU&SspKafSRe(s8x-kkD}r$86e1a zTNPM_tVId)hvLzOHGf@zdw3+#uBW2NtzM?=L`7pW(FCP^cSO4Ses_J|c6=HY0fOE4W(|(34-i_3lgik@M5~ z;zZG1+*5wo(a+A(J)tf6mez0Mt>-}=LgjFqwjuOIBRA;+{A@-20v!`v+WWRjX?Fnt zt+UYfF}H^b+7sNv($d~toI9*M3*Y>mgL8+BSUJ7vQ6|9L>zOYyo?n)q909U#*}CSo zRCyu;D0WZ3q}qpoZYl<*{ZgM7VVe@D11@@4%z1gbBnldR$YIBEsMpCtpQF5it6Cxh z67f)2`H6{4Rb#`7K@t?N`I8M+U6e*0S9Pw&n$Ue}SlN26H*2fiwq&wvXpniAi5(~{o*#TEa z&24h~(5yi_IfiLm6mqZcK@oZs_8SVnYBJ&gKTo+~)9)S&nXT)It3Yxs?yPOK+W&x< z=n%(yjlgO5RUv3_2r1y<)#n-=sitn;i#U*@Lv95Xno~~L)pu0ck3k<0DZD?uv3+LC+OKOf z2`re~D(4yJdu3a-pXrl}Y2SY9_aAy6Qn~9bhVX!XZNn)W(_oKF#YCI_l2qXjT{dkC zp#QPmCD!U)Tim)der5cB62}P}hQ*1lYXSI-O)Gx%H!Bv^KeJ3 zXAf7J{Zq#gKR7qFddF>!4ya%DD~d@QQK(0u1PvCA zC2hCkCfh7gDh9v4HYOWjD1+|3HmS~x)`R1Qj=hUfysi4%~tD6^jrqF8c{U)GDzb1;6gn(}`2pV98} z)+_q4c#6}=71)#Jv8L|0*0U7fR?nR+?C&#pi1Fgp%7@bFG1`jGb{#{`CyelL(o5v0^CNkuy%0+z+@!($*vpfk%(1eNjysd)rHRU*n2*Z_{n z_nA4hIVJ#7c;|NmEW~U~9=bPIWN04PDuR$hym-gATh(P@3R75KiiiNch8>`Z!97?? z_Cf=A5)2jjxVF5Gock|tj;2C}Havl4Itw^D){;FwjI`92>VVw*3f*)aea1y4)d$^@ zhVvj?Aq1MYw4Q|Q&P#x8A;A73$K(Apm zaxYh1BI>{K?L6ZYeoppEFs{SiY5e=t)tnRo7F`V^tP*OW=2!qq)8TFe-Wi7DJlHZNi{UJAIM}`jXo-hk}=G#$NI>6N0NE_orQyS z23Gel{InSD{mpCE7)qT$#AD!xx%2T)01CNkN==|wK$H2bw{`jtO%pU+*r`b|`p17H z-BYw~8FwfqO!!c*To$C+>SD*>OxP-W!b>EAszZ?92k$eW_1F5Hed;Kj96lE=8Q<~; zWtWyn9Ud>31tfSGWlyLL}W@6eEb$WwqQQ#G_10a z5*E_a0UQ6+I~YKs0mVTx>+`M zAjX(DfgDzzR`+m)%8qsi$m@N5QEV;XnPJYcoc09N4Z$kd?Bq8{SNxSUo}X{L5Eew~ z>4K)pW&AvH$`zGQ6^l0eNv_YZg>=73O#H>^be28iJZN<2sgw_)53U_@UWRJh?wzjM z|J@l=n`cC=xS}jvENPRN`QrO%uFpCuYdVKqD!GYN1(fswOYZ`w|AU0t1c_>MQRFpJ z@QJA%A;9a%Ye#f@i@wDI;*4aLiNA=yu?K9KEIUbhg`e~eUPpZcm^^xw?LB_R$VHmy zJCNCuZ6K)@UnJ7LBsML{p>sPX+*gmZjej2H@_MS-eN$Ge?1tT(a5$d5kPVhFW1+Y7 zZZIhyb?z7LP-LZ}+5?h&m{pLp)bkt){ zCBtQq-jeY?L?eI2J^MOW)_uLI>#x*bWy13No(c|{7b6f#)gwR&S>@>)3pi(W)z}ic zGB@sDVqc&aufGOV=V$2F=>~#}N20D*O*W{8msFV{u`gLvG(loqsy%y;g8SLe zXsn1r*LXqyI1C%TcR1m85onAlAp3UZBGoUnAg3(~RR$Snu|PHPHz59T^e^wTk$Z!F z(-_2YqU>yIv%5&lSLXtAubACsaKI|X;L(;3C41(6fc=M+>o?*1xI&c}n7FAaqf~>T zD^Xa0X;Qq@U+=^1Mdh)n(2bZF*NR}&$wYGA=jL5vhi|6Z>j|dst=9Lcew~tbx9hpN zJ(C9IdI(!?L|%e>z*e{^DU7g?mTyQ6F`b{{@J#h`Vr*djvZw}>Api~qFyCbMd#tu1 z<4_paXQ?S}H)OVfEaXV;c=R5Yva{EK2~7G0p@TnNobac%KCuhXtMWk&N+=Dxd|Y8V zx8m_zVZ3}y<-&ANaPAGUD7hTMBaItOLsoUa?fZ*TkN8GV1D7J-k$;0ex=!(FJEx*G zk{+`z(14$c9b4Omkq+Bc9C5N_rur9;#DszA>^jj3!DXqDga{7VScvwxRVj*swo zu&-B_TMj%Qx*HId4P^$rsqzJeL|a8h|MJijNKcoF@iZaA-SK@!nFMgF|Lcm-HPdHc ztRy6c_xE15G}S|x#6@hgz7o+EnZ}wdqulybfJ40{tQ^tjAZ03SY_jsDX@MF|1%xLW z8nN8{zjMQx3A8sOD9rUc-g=54$>&U6_}cL_{PaGtQ2SMr$+O2uVNskY$1p5*YkV4v zk_=>avx0@;!a}vCLe<-^k2nSUE3!LV_M|Vf_Ul5MWn$?{4l`|ZkaW#T;)Y-s-GpGr z=VSN6Rt~x!bMA^K-}l6A*l}T-AN3uW)luT^2=Gh_0KL3%A6F2s%`_O2Oc{7T{#r_t zWS}iH?e?&9<0<5kxp({l%OWu5Y<7+=zcAyXAI;~Ptho^p=^_lh3fC#EEW#9=*&h7x z>m;fKOC$Gim1s?mUtGCGWGC3wf}JqRJ?xbqH8eayn_;1T-ud=4QzG(6JI&GmB9N?$ z5S`Y!_E`LZ*0~%0Cy`=N+SnZFsW_kY4^B700f+IUK_Rw=gBhJAW|yF?zLKRP6mS!DNx$iZg2F>t2a_=?Nzr zpwjgD-L_x!qJ@XmHoV%6OLxB2)(pZEc((sp=zSF>(LM`jpM$fHVo}P8hgap>pMw>I zl2#v~C5FulX(%aiP$kjGMzCa>;`8G>&Ul;fUKx(xSuHlx;uf^HR&YsSyR+?Tre4H4_iSO8a#^$_yZWC)bIvC(~@!36h$2E%EqSOyv z_yfc@536yXx_K(W04rt)kIHW!M#Zzkq08NWUzR^+b^LzRmX47-2HbWsRh~?I{1@@L ze#5FOT0J`YgeYr8wvR(g9cSLKW>3(0Z{76GbRD-07{2uOU3f>R(M7@u9#BO7Z3F)Z$gO$?k4 z@bp&u1zTXbMD{t`*=dVYIP3h)cNC|*7&w1$U%3}f#r*A*f=8H>sjYmHPvv0Oro6~> zLEzs`6s%pV*b1nXK5{AUq#$XEyhPR?CLe(*X044EG1?>R$N5>*NPft6Ff16!%QK|=)elD+|pz!;XXO}*M4=>0l*@L*3P%eeR{D^xv zh}=0^w1kd!k)G{VB>4z$3KT6Dhn+$gQ*c0_zxCTH&UbuZKbZ|*|W`7`#c=jgQqv$>Sv?1R0e?DnhH zEa@hXg!+%|8o68K?jmKG7ZRUdP(G_RXK?+{yD{xZR>D^o|B~=^P(J#v**^)QO79A_ zpvij&RezP?wsAH5U^_W-_6a;Si1xLa$-G%_*vy==BHRiZ=&;*`j-k%KpTHD}<`-a7k_xid{djv!h5 z^az|MZZZahGC2{9gCt)$B!r|ddwol85d!Y%cQv>4@$4du6bjXVT|$zTh9H640JC^A zp9zOE1lO>CnS1w5>f5TsR#lqOMhHLQeAMl|)C4JpejhreQL=`L2|iVX!uW93M6pVj z{cHF4MtA6mCb1cv|7yJ~-{-P|WteGk+M|z;FizLDQz^^Oe1)H&OHwgSXtw2mSuXk_7F{N&H8vvK{;?CQRN>*+q(O^u@B5){B7o#`!O2| zkl@_BudFgD*lOIo9MG$%l9N!>z+WT_i_9K5G_LgIsP)2d82@$|=j&_5t=JuU^E8h_Q>l0~GJFbf*?&LCz8Or0od`5XQ1F2#uO zb|>;xxWjfNeL2iIi#LE9Ti%cGXNsT+fJIs|CJdV)yG|tCXf54iqS9aUwn`I0a(=sW z`t~E{5kZC!d`bTQYh`(#Fy(9EW(`o!nO8q7ngVaQ1+4r+W``}zsxMJ?Q`G9{0r&9t zN`=?ED;CH%(houu>LiL%xC&Z+-eS9l=oeQ}@kV|XOSlKW_gzJ{q;_%zAD^M0*2Dn= zWo$ilGfVX+`36roC%)t$Lm_8la=Ya3Lq%cgpoK8@8tB1ud)b znS)yI+*rks7N1oP{|5BOGERnNW^ltUqJpZIo}IJ#O#7CgyPD>GIbA75Q~5NB4~0^m zX6Gli8njBb%_K6VCSp*gBuDh%vBna*O>0)le=vBiqE-@2N2=)j;p|lG*~TKWpaH@~ ziU!)w-={770w$?3H{gouB2qr~XE<2n+Wp;Wr6nWulJTLO>>Hobjz$$CAKCPI(w$?# z?_OvUeGd#(+{c23OIY=)niXp6Hc9o67Jc)IO;dodgix{&JxkPlF}wP20A!m~Xvxj= z2YUNf_?Txn=x|QxpZB0?2D#IIPoY4Cl5)&zHY|4x6Fxk)G$jJ?rnhxcV^oSiI(wTw z+`%8~!Z=At*4XHbE^lqVZHVT39D}!B$g|0q-hSa&g!Uc)D!&hgvK*h@Gs|!U;VrC% z0c6R8Y0qYE!75;17E-)FUZuTBk{^7ezade!LoM4(o?RqKd(L*dGQ5K-PdyMQku2n% z0r~}oHhH<@{?*TvtI?o{kW&^>saVBj&Hw_>5%*f9br-^ID#rr^(lS4Zt(ewZXFRt% z@|`0M$`tM{=&JA@<$A6soI$8?}2KOwoxElbT%MmrB zJFuu!{}o}!QY9zu&CYh17%P7)V!bX?tx)D6&mgM`n$T@Mr^_rMv+n&y1kNVv-)hCt zWyF&5CQgRu;=G_-M;b4&(NU(#~y&h-z(&OhZC7eeGg|*7%7PyF?irBtls)l5j=xi2&&K6oQxG_Ma zo~e4OaY_An(kx&sF^8KIF1IhX)|zbxLAbT9pUrYSkC&dJmyD9%zwZZqgyeJa7KPq{ zC5!bAH9hsW&fk6a^^A8DZ1~SdHo-Rm(SvQ5e38EAj%44o1X2((jslOMm0VXYzx%v# zq;m5X6n#z%@h8uRYLUtVQ6^L?+^r!a%~d|s9&91NA)TkYyin)Gj_dOBROEvP6Jsc` zy#X9Y<))Jni-t<@G!$MP_GuQR7hhBf_M0KbjOL&~NBiWGG?}ZZq$gy%F;=;;LHez@ zIA=>sJ^Q1R$p94~uz%$w=mt+F_IUJIsMCz#G_YB009IR|$@$+H*SJv8fe(fhZXPxw z!VYh8VX#i($%JC;;M-DITcaM{4%-?tqhMHmj-xmymU|d%Pzmx|2$WFn*HECwA-CtSjKf-BVX;J)75os4@CnG&DqK@FR zPK{+sVw}YS7IyuVnxP-rB%$I12v)kh-SJIvXL=jl z37@3@&l#tCFqx$|q?>?LEQxwV!AimA6xP=3a;|tOLdgY@wk&Fl)hN3Sj5moRm9hR{ zW`D+R&vDP#D0Gr=JxWblJVstLELyBH#GlT=%I~Za9Fk30f-kY}Ng_$&hJw|8hMl$M eoV>3^VUh&YfBC&dMesCn2~bzoR;pBhg#14WYe!4~ literal 0 HcmV?d00001 diff --git a/app/src/dev/res/values/strings.xml b/app/src/debug/res/values/ic_launcher_background.xml similarity index 52% rename from app/src/dev/res/values/strings.xml rename to app/src/debug/res/values/ic_launcher_background.xml index ede9730a..f2d61e27 100644 --- a/app/src/dev/res/values/strings.xml +++ b/app/src/debug/res/values/ic_launcher_background.xml @@ -1,6 +1,4 @@ - - Fast Dev - + #F44336 diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml new file mode 100644 index 00000000..40a4e868 --- /dev/null +++ b/app/src/debug/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Fast Debug + diff --git a/app/src/dev/res/mipmap-hdpi/ic_launcher.png b/app/src/dev/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 037e657b464eca435746e172983773b901529340..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3313 zcmV&P)2B0p+sZV$IQZHKmF56!c-?W1;G zPb;aVX`zw%NK(^$R$BRB7*aD*LozI*(!TwE-@V^(=iUKkn0p88f6njBoIBs+|NqVZ z@qORDGk$*ED1;Dy5drOheJ=0!s}<4UUKG9YD*wDak@or35hi*9j0=!|2BFvcQiM?q zr-&1Q_qf(q-X7A~nAe0@t)!R5Qrla4?Kab~L1pIpoN`nBf^xIHs@!DXU2bl$Q#6&E z>%XVhe`LUWc8z|<+SscqO!W_;FUF9Li8(Nr^jeP=ft0o|C^a|SP-beFQ(>}ipoX5K z#uK2-Z1V`bpEyP}#>CidGZTOL7t*5y-(sa-o6DD)Y?)F-Ay=s6=5oT}39GHvG3mtl#M@hfAiZM0|ReURj#8X5v7&%Js@C42T zi`N9QwqkBqI(3`i+u~j`%XBh^D!EocjO1Ha9c*j`QEQ=#TtA=7UHXH8{C$Plejior z2SyB6T1#T&kj+W+T`P2QrK?YbPiWQK`%_Zh;vQ3KuKQV`ypO?k5Ok`qfsRt0H5We7 zEJJ-V^_r7V1WKuqU(^D0kq~vVTz@P@s=H>w6CZWuNpNFMj$FZ>0pc~EMr+rl?{C1Z|7Q@6Z~LA6@v%*nBnSC*c4B1db2*hZKZn_}aU%lT;=e=D`w zE#ZyJO}5vx^q$+Z>&f#EJBOAp2m+W*w5k|0Mw^>>>f?rz1#qvKgg*Kkkur?OLI z_20i%kmC!20BYYaCC1N4o^I15sqGyKPgR;`_f;km7TKa4<*hoN4@oOirmDFXh0ca2G2v9bcxU_dHT@ zy4}zM*8Qi6tY6lo=%)>EOj1V*)84O=a-a}?hrpV?b1+TLP3Wve<+_cod0t+mtXfiR ztZF8kRyQm9Y5_+@(|>fq68~(ZlnW=pZ4|r~8$Ov0?-9;}l>>T%+p;?kt##CM_HJ(> zo7a4!=&u!EYl-nF4YQhiq+B=&5pLx9mza*esa1GAXgx_*b&!}}f2oOVTX&Hhc<+)= z3F5XErtf&0a)5ho&UuK4cPl3)tqCnP*}g(!wZrR<@iera>o~M(RyC9T+b{W?ARcBj zG7g^8M?E@B$_XckF*aSlH%B-YP`7enm;U6uFBF7Gv6gzz?#(TX9A6UzU`>Q-c5~wD zJaAE4D@oQo=B$Q84`~rz56wJ9R=v5YNFZTtyt*?O#M;=0~iSgEpCg@#`#)<(v z2gq4?&lv?Fa%_Lo*?YQ+AVkm9^j#lNF8Vll;mOEiv0PVTI%Y#-wMzVbQ$HtFf9*Ii z1GY~tlg|!*Pmb*U&Vdi8M?L>Yqgy}-%jfnK>=rmp)YCHd*b@>Gg1PfJ7DX#otQb;e zt~eZ`V#y7M-h?48z33w$d(jPd#V8Md61tTYD)22<) zl%Q{UWBCA{096ZzU~(ebgSP|$d$zSG^zZ}@f(#$EEM2-ZK|xSWP0h$sa~(Sz^R~>- zcQdrqBxuW8FLRc-akUygyi5s7RS;BNU7es7LC-zl`Vg!=cuUaNU;W6|S#mu*f`p(Y zOP1&q1g%`TGPcZoq6Lk;4Y3#ha#le?NAQ-QWlwv5G}4xX4qd9OtQ;*Rh*hzL3l|P6 zGaXkYNk2O4GrV@Uw6=Wx>aGgI}k$ajAHJtfS38g^!FP=LPA2F2og!^S8O=^zR*FPg`&GSUH^l^ zcy|Er!*W7CUe_Qj6bp3Q_E9cEB)mAb8rRRz?_bFqD+kaHT_Nbi;mhpsO+HrFA#62r zlQ+B$E<{cQA;QM2Prh#JV_IxhJAUUnTS;e7Q&Zw840v94D4uZf|@0`S! z^8|vF1850P2|{RLpC=!u%b=daj$I9hi7{qjF}SFe9d{ts0%8VCk191DZA2$;E14(o zgrLS#t>mtoy{}qMFp2xHwB2VTf^QlLE*$xXi^y32qKthTg^s=pGj|DU{`x2K)V=Cd zEoboj^Trm+!2pp9XAxsA=+@-*^U+C-QtvL@CFt$d&Ftt_-rrjwT5y^;KK_M=!NI|U zE*`EDT`DkYP+nq*;plnZQ9hU!aEG8XpSQBRvGTrNgNIl8?xu(VWAR{Bpd!4uBqde$ zp)a9R(rQ&dS?ma%5%gWl&*a4?&MNw;0pY0i1i5AO>b0<4mnUBz5TBMXc2S#$iMDqvB7?!En8&T9#$!J5qV@Mav>|VWw70nev2rZll$lIy{$HH(|OGMaBIk45@SJ`pyk&DRa(D5T*s>D-R z$o7hfRDztKA6u`G<%_&O1ak>tTXCh>@G;4ZUGy|;jEuBiFSp+v@ed2j!tIaP$tfE* z!yVz4iV0N!LP<(c?Z%7Rgcc=-CT20TR=rDYixX{hz3%BuM7JB*FK~!%TG~!KCeb1v z;n0hF5ngfVFK?p;*sqdkx}dqWAt*3R58L#9ce$6C+mrC{f*|^GLu}aeycrpLTM)Zi zmHyx7f3D4XhXDs-c2b!3-lgb41yf;@Xq)#EADSCNUn0|o-#^=$agZfAtw_=a0y)#u z_FUCP-2FRh%W%=Au7#Eh@sA#089-m7^)Zj#Ta><^k;HNmi)*<;Jp|$sg_DyJ{lFh# z!-)Rb_??Do{uJ@1oR(-1W%e;RZab=Qm> zE&-7tnfMl>0s}*g6O&%sLKl+o+Bv=ql>0IXPVX z2M*NZ*TfMN$z$W5SxA=w7x9gC3sDkcphIEWj*DYrpL~Yuh9C1rh`Oqtb2=0Ma6f%e zP*4;edK@_{bljwy5?8!clySgu2+mgDT85smWndLb_+iQcG9lrAHN!$P@dMtG&<#35 zSDEfUmEOU@qY&F8D3V5p&%JZv=rub~>4eh&MieuIk>!nNp)Vtxm2IJtQjfu0m@_ea z_8jN{T|}Kc+h1uY%$xf4i^0FK#8RXt3@NS1Nm#ySs;>4dZl_4Rig^|z42I(D+gK+g zc`4{M{^usVJ@1dPiVa5y#>N~Yhm_#B0*|c3LWiEdghZ?PqX$hXZo#hu{?w(%L~iYdh2UDY5E5Bva@Zw~T)E9gK;wF$dFr584VqDKv+d!+A{*BbR{q`1ror+1UeQVi;L? z_AicN6h(4iU|>3hk$yIA?AWmrCrp@-3*bGj(a#KMgSKKH^o_(=7!zY-4##ZQxCA66 zCH2Y5$_mcR%nVIUO&v0L@Zh2JI>HgeA<0LHz-Y5KyF&)=+7eewJzo3TQwH7}F3x6`~cBCR!6p z)uau90;NU+h$$f~4IfKcphydJw+ku=6w0^JKxjMO^Jd<5n3-*NXBTdA>^O7ZedoM) z-*+*Yf;3SS!w3Tj;rOfmt~RAF%BsIVFr&%`5C*B=!{~hkA(Ajg2A(6-elWy9EcN^Q zX2m9Pz_e+Odx*a&c*f7Odgj}$o^|sq?!9)a+f8V<3vJgK@J#QdZ{$P1D5H#ln24=> z7jj!@c82q3+q9O50_&MVB34&mZR@mK+r$E^I{=^z%AP4KOuF=tUL7GcG0bM;`7MUo z1<&GqtGo3c(t^9kZgF?}1NbV%uviomu~CE6!yy_Uy~ItX!pUc?)M^c!IHgB-zSVUX zf3+>#H}uNdt*!>tp;xydQwE^iT}J5Ql~W3^=g^?7iwi*=)JV(pI0R(Mg4E?}d8?z< z)F}`WP)rtE{(rl0dYv8@zf3tB*ai1SkVgx#{8d*RVz6-{_eQ>Os+uUUu@HcW-oaBK zxXKxMMbHNUWn<;0HCLEq5q=G9KF1eWT_1Q`-XQrOzibz`R&dPIu)bAADZPQDe%y&lg+` z2%sEPf#BWO&UncOBLLtWY-vr6M2&IOU9oxh5cx$sb&Sq7ARH^i2w<0JxHV1lXkx+3kdi2z+B7mAK+wU-Y;8uK|{W# z{EE1L*UgX$khbD3F)O9MiYT+AEV#qM@`UDnUMHQxzOcgfr-nSRZT(e7KI8)E@0`?z zgT%RaDs?y<5xK%ix6;WUyfvlGN9uHldw1Lr5AC_dP*-z}t;~8Tvj3%t>kE8k&Ne$e zB_$;zy#kt=nx3Zl{}Ojn7xV?a)Zu79e@E=@?)D2iH(k@j)&mz{GaqlStgMXk3UE4| zn|Ik7qK@key@R;XQ&?E|gjWFBkhy8c_H!q7K_ft3s|l-7 z`r$vCa(#hwo1MI`fjAFm%;indAevh%l}^E;KLLTH4hQ2#3t2|uZySg+?pFu#twdO6 z;?5<^5nBlZ+X*%L5)f$WFfdNEm9U%=Ux{760-W#JN<0@`nnDBlCY+6I=G5p*K>6pF zH2(cUK>mAhGP{);_0eQ-b~qfY2l#e6BHMg;51fr|rMCcGw{NC%lZ zm^OQIEzRQ83FT|RNjG)0hN;PhE*h1d@bgBTK`9U}xdIkp7!vtzG~kT*O`E|*;^fEW zE=(Q|=&CWZCT`m%_XL6)N~Zw9(gt;=PyA^IaTp6OKJ(s}$@lR?2; z+c0@^sdt>6&EdPfEMH)Zm{s%kSIxsj3hYdvBh7GffH zzw>Am@xM7jEomzNEWi|K{zF zx@Eau$TVI3hi#Kg%WrNMHk!OofWDOmXEjX@3(~t zdMhQ%r!~UKZ~g?m3HlSn6KE;$9^c_Nf7^6S#AR@|M~Fs~pU-v4%G!Mgp{4~H`P!!P@GZsR*}iFvpBwl7^&39>@!M zipxNmC>wQX`TZ>*_8MN0Ssz}UVV*`vEG0NhaD{*?Hkqv`GSxZ_aXRER-B|Z8K5tS6 z$`Y5|+!1baK|{sF|19%Fx9KKJ9wBBOL4%m^W-`r<MXYfmYB zS?f?X>foq;H;vdftjJIkJw3xxLQF-ql=NnTi;T7ZvF+LI2Fb%lLHzg4Wm>BFx{x|K zs;>u&U}xEriv=!GU|#t+iQyl2

v{*Vv*L>gC6Y^t*~6EC&R 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 diff --git a/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png b/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 8b839a55ca0c14a6876bdce21e78a8d910bd4e77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7322 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 AlK=n! diff --git a/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index c3b67b1692adae8c8aae3a568dfb7e9946217fe1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10692 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%0D6%3IP1^p&WZeux`vldDuJ8+ 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 Z+pS_VbYHpQ_3uo8lAM}sowRxQ{{T(osW|`u diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1d0023bd..2cf96e8f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + @@ -9,29 +8,22 @@ + android:networkSecurityConfig="@xml/network_security_config" + android:supportsRtl="true" + android:theme="@style/AppTheme"> - - @@ -39,10 +31,8 @@ - - @@ -53,33 +43,6 @@ android:exported="false" android:foregroundServiceType="dataSync" /> - - - - - - - - - - - - - - - - - - - - - = Build.VERSION_CODES.TIRAMISU && + SettingsController.getBoolean( + SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, + SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND + )) + } + } + + if (isNeedToCheckNotificationsPermission) { + CheckPermission( + showRationale = { + MaterialDialog( + title = UiText.Resource(UiR.string.warning), + text = UiText.Simple("The application will not be able to work properly without permission to send notifications."), + confirmText = UiText.Simple("Grant"), + confirmAction = { + viewModel.onRequestNotificationsPermissionClicked(true) + }, + cancelText = UiText.Resource(UiR.string.cancel), + cancelAction = viewModel::onNotificationsAlertNegativeClicked, + onDismissAction = viewModel::onNotificationsAlertNegativeClicked, + buttonsInvokeDismiss = false + ) + }, + onDenied = { + MaterialDialog( + title = UiText.Resource(UiR.string.warning), + text = UiText.Simple("The application needs permission to send notifications to update messages and other information."), + confirmText = UiText.Simple("Grant"), + confirmAction = { + viewModel.onRequestNotificationsPermissionClicked(false) + }, + cancelText = UiText.Resource(UiR.string.cancel), + onDismissAction = {}, + buttonsInvokeDismiss = false + ) + }, + permission = permission + ) + } +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/MainGraph.kt b/app/src/main/kotlin/com/meloda/app/fast/MainGraph.kt new file mode 100644 index 00000000..8253d8b1 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/MainGraph.kt @@ -0,0 +1,196 @@ +package com.meloda.app.fast + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideIn +import androidx.compose.animation.slideOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Settings +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.IntOffset +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.navigation +import androidx.navigation.compose.rememberNavController +import com.meloda.app.fast.conversations.navigation.Conversations +import com.meloda.app.fast.conversations.navigation.conversationsRoute +import com.meloda.app.fast.friends.navigation.Friends +import com.meloda.app.fast.friends.navigation.friendsRoute +import com.meloda.app.fast.model.BaseError +import kotlinx.serialization.Serializable +import com.meloda.app.fast.designsystem.R as UiR + +@Serializable +object MainGraph + +@Serializable +object Main + +@Serializable +object Profile + +data class BottomNavigationItem( + val titleResId: Int, + val selectedIconResId: Int, + val unselectedIconResId: Int, + val route: Any, +) + +@OptIn(ExperimentalMaterial3Api::class) +fun NavGraphBuilder.mainScreen( + onError: (BaseError) -> Unit, + onNavigateToSettings: () -> Unit, + onNavigateToMessagesHistory: (conversationId: Int) -> Unit, +) { + val items = listOf( + BottomNavigationItem( + titleResId = UiR.string.title_friends, + selectedIconResId = UiR.drawable.baseline_people_alt_24, + unselectedIconResId = UiR.drawable.outline_people_alt_24, + route = Friends, + ), + BottomNavigationItem( + titleResId = UiR.string.title_conversations, + selectedIconResId = UiR.drawable.baseline_chat_24, + unselectedIconResId = UiR.drawable.outline_chat_24, + route = Conversations + ), + BottomNavigationItem( + titleResId = UiR.string.title_profile, + selectedIconResId = UiR.drawable.baseline_account_circle_24, + unselectedIconResId = UiR.drawable.outline_account_circle_24, + route = Profile + ) + ) + val routes = items.map(BottomNavigationItem::route) + + composable

{ + val navController = rememberNavController() + + var selectedItemIndex by rememberSaveable { + mutableIntStateOf(1) + } + + var isBottomBarVisible by rememberSaveable { + mutableStateOf(true) + } + + Scaffold( + bottomBar = { + AnimatedVisibility( + visible = isBottomBarVisible, + enter = slideIn { IntOffset(0, 400) }, + exit = slideOut { IntOffset(0, 400) } + ) { + NavigationBar { + items.forEachIndexed { index, item -> + NavigationBarItem( + selected = selectedItemIndex == index, + onClick = { + if (selectedItemIndex != index) { + val currentRoute = routes[selectedItemIndex] + + selectedItemIndex = index + navController.navigate(item.route) { + popUpTo(route = currentRoute) { + inclusive = true + } + } + } + }, + icon = { + Icon( + painter = painterResource( + id = if (selectedItemIndex == index) item.selectedIconResId + else item.unselectedIconResId + ), + contentDescription = null + ) + }, + alwaysShowLabel = false + ) + } + } + } + } + ) { padding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(bottom = padding.calculateBottomPadding()) + ) { + NavHost( + navController = navController, + startDestination = MainGraph, + enterTransition = { fadeIn(animationSpec = tween(200)) }, + exitTransition = { fadeOut(animationSpec = tween(200)) } + ) { + navigation(startDestination = Conversations) { + friendsRoute( + onError = onError, + navController = navController + ) + conversationsRoute( + onError = onError, + onNavigateToMessagesHistory = onNavigateToMessagesHistory, + navController = navController, + onListScrollingUp = { isScrolling -> +// isBottomBarVisible = isScrolling + } + ) + + composable { + Scaffold( + topBar = { + TopAppBar( + title = { + Text(text = stringResource(id = UiR.string.title_profile)) + }, + actions = { + IconButton(onClick = onNavigateToSettings) { + Icon( + imageVector = Icons.Rounded.Settings, + contentDescription = null + ) + } + } + ) + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + + } + } + } + } + } + } + } + } +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/MainViewModel.kt b/app/src/main/kotlin/com/meloda/app/fast/MainViewModel.kt new file mode 100644 index 00000000..5d8b82c3 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/MainViewModel.kt @@ -0,0 +1,147 @@ +package com.meloda.app.fast + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.meloda.app.fast.common.UserConfig +import com.meloda.app.fast.common.extensions.setValue +import com.meloda.app.fast.common.extensions.updateValue +import com.meloda.app.fast.data.db.AccountsRepository +import com.meloda.app.fast.datastore.SettingsController +import com.meloda.app.fast.datastore.SettingsKeys +import com.meloda.app.fast.datastore.UserSettings +import com.meloda.app.fast.model.BaseError +import com.meloda.app.fast.model.LongPollState +import com.meloda.app.fast.model.MainScreenState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +interface MainViewModel { + + val screenState: StateFlow + + val longPollState: StateFlow + val startOnlineService: StateFlow + + fun useDynamicColorsChanged(use: Boolean) + + fun useDarkThemeChanged(use: Boolean) + + fun onRequestNotificationsPermissionClicked(fromRationale: Boolean) + fun onNotificationsAlertNegativeClicked() + + fun onNotificationsRequested() + + fun onAppPermissionsOpened() + + fun onError(error: BaseError) + + fun onAuthOpened() +} + +class MainViewModelImpl( + private val accountsRepository: AccountsRepository, + private val userSettings: UserSettings +) : MainViewModel, ViewModel() { + + init { + loadAccounts() + } + + override val screenState = MutableStateFlow(MainScreenState.EMPTY) + + override val longPollState = MutableStateFlow( + if (SettingsController.getBoolean( + SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, + SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND + ) + ) { + LongPollState.ForegroundService + } else { + LongPollState.DefaultService + } + ) + override val startOnlineService = MutableStateFlow( + SettingsController.getBoolean( + SettingsKeys.KEY_VISIBILITY_SEND_ONLINE_STATUS, + SettingsKeys.DEFAULT_VALUE_KEY_VISIBILITY_SEND_ONLINE_STATUS + ) + ) + + override fun useDynamicColorsChanged(use: Boolean) { + screenState.updateValue(screenState.value.copy(useDynamicColors = use)) + } + + override fun useDarkThemeChanged(use: Boolean) { + screenState.updateValue(screenState.value.copy(useDarkTheme = use)) + } + + override fun onRequestNotificationsPermissionClicked(fromRationale: Boolean) { + screenState.setValue { old -> + if (fromRationale) { + old.copy(isNeedToOpenAppPermissions = true) + } else { + old.copy(isNeedToRequestNotifications = true) + } + } + } + + override fun onNotificationsAlertNegativeClicked() { + SettingsController.edit { + putBoolean( + SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, + false + ) + } + userSettings.setLongPollBackground(false) + } + + override fun onNotificationsRequested() { + screenState.setValue { old -> old.copy(isNeedToRequestNotifications = false) } + } + + override fun onAppPermissionsOpened() { + screenState.setValue { old -> old.copy(isNeedToOpenAppPermissions = false) } + } + + override fun onError(error: BaseError) { + when (error) { + BaseError.SessionExpired -> { + screenState.setValue { old -> old.copy(isNeedToOpenAuth = true) } + } + } + } + + override fun onAuthOpened() { + screenState.setValue { old -> old.copy(isNeedToOpenAuth = false) } + } + + private fun loadAccounts() { + viewModelScope.launch(Dispatchers.IO) { + val accounts = accountsRepository.getAccounts() + + Log.d("MainViewModel", "initUserConfig: accounts: $accounts") + + if (accounts.isNotEmpty()) { + val currentAccount = accounts.find { it.userId == UserConfig.currentUserId } + if (currentAccount != null) { + UserConfig.apply { + this.userId = currentAccount.userId + this.accessToken = currentAccount.accessToken + this.fastToken = currentAccount.fastToken + this.trustedHash = currentAccount.trustedHash + } + } + } + + screenState.setValue { old -> + old.copy( + accounts = accounts, + accountsLoaded = true + ) + } + } + } +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/RootGraph.kt b/app/src/main/kotlin/com/meloda/app/fast/RootGraph.kt new file mode 100644 index 00000000..806ae91b --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/RootGraph.kt @@ -0,0 +1,92 @@ +package com.meloda.app.fast + +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavController +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.rememberNavController +import com.meloda.app.fast.auth.AuthGraph +import com.meloda.app.fast.auth.authNavGraph +import com.meloda.app.fast.auth.navigateToAuth +import com.meloda.app.fast.chatmaterials.navigation.chatMaterialsRoute +import com.meloda.app.fast.chatmaterials.navigation.navigateToChatMaterials +import com.meloda.app.fast.common.UserConfig +import com.meloda.app.fast.languagepicker.navigation.languagePickerRoute +import com.meloda.app.fast.languagepicker.navigation.navigateToLanguagePicker +import com.meloda.app.fast.messageshistory.navigation.messagesHistoryRoute +import com.meloda.app.fast.messageshistory.navigation.navigateToMessagesHistory +import com.meloda.app.fast.settings.presentation.navigateToSettings +import com.meloda.app.fast.settings.presentation.settingsRoute +import org.koin.androidx.compose.koinViewModel + +@Composable +fun RootGraph(navController: NavHostController = rememberNavController()) { + val viewModel: MainViewModel = koinViewModel() + val screenState by viewModel.screenState.collectAsStateWithLifecycle() + + if (screenState.isNeedToOpenAuth) { + viewModel.onAuthOpened() + navController.navigateToAuth(clearBackStack = true) + } + + if (screenState.accountsLoaded) { + val isNeedToShowConversations by remember { + derivedStateOf { screenState.accounts.isNotEmpty() && UserConfig.isLoggedIn() } + } + + NavHost( + navController = navController, + startDestination = if (isNeedToShowConversations) Main else AuthGraph, + enterTransition = { fadeIn(animationSpec = tween(200)) }, + exitTransition = { fadeOut(animationSpec = tween(200)) } + ) { + authNavGraph( + onError = viewModel::onError, + onNavigateToMain = navController::navigateToMain, + navController = navController + ) + mainScreen( + onError = viewModel::onError, + onNavigateToSettings = navController::navigateToSettings, + onNavigateToMessagesHistory = navController::navigateToMessagesHistory + ) + + messagesHistoryRoute( + onError = viewModel::onError, + onBack = navController::navigateUp, + onNavigateToChatAttachments = navController::navigateToChatMaterials + ) + chatMaterialsRoute( + onBack = navController::navigateUp + ) + + settingsRoute( + onError = viewModel::onError, + onBack = navController::navigateUp, + onNavigateToAuth = { navController.navigateToAuth(true) }, + onNavigateToLanguagePicker = navController::navigateToLanguagePicker + ) + languagePickerRoute(onBack = navController::navigateUp) + } + } + + NotificationsPermissionChecker( + screenState = screenState, + viewModel = viewModel + ) +} + +fun NavController.navigateToMain() { + this.navigate(Main) { + popUpTo(0) { + inclusive = true + } + } +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/common/AppGlobal.kt b/app/src/main/kotlin/com/meloda/app/fast/common/AppGlobal.kt new file mode 100644 index 00000000..f76e8f06 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/common/AppGlobal.kt @@ -0,0 +1,35 @@ +package com.meloda.app.fast.common + +import android.app.Application +import androidx.preference.PreferenceManager +import coil.ImageLoader +import coil.ImageLoaderFactory +import com.meloda.app.fast.common.di.applicationModule +import com.meloda.app.fast.datastore.SettingsController +import org.koin.android.ext.android.get +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.GlobalContext.startKoin + +class AppGlobal : Application(), ImageLoaderFactory { + + override fun onCreate() { + super.onCreate() + + val preferences = PreferenceManager.getDefaultSharedPreferences(this) + SettingsController.init(preferences) + UserConfig.init(preferences) + + initKoin() + } + + private fun initKoin() { + startKoin { + androidLogger() + androidContext(this@AppGlobal) + modules(applicationModule) + } + } + + override fun newImageLoader(): ImageLoader = get() +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/common/di/ApplicationModule.kt b/app/src/main/kotlin/com/meloda/app/fast/common/di/ApplicationModule.kt new file mode 100644 index 00000000..411accb4 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/common/di/ApplicationModule.kt @@ -0,0 +1,47 @@ +package com.meloda.app.fast.common.di + +import android.content.Context +import android.content.res.Resources +import android.os.PowerManager +import androidx.preference.PreferenceManager +import com.meloda.app.fast.MainViewModelImpl +import com.meloda.app.fast.auth.authModule +import com.meloda.app.fast.conversations.di.conversationsModule +import com.meloda.app.fast.data.di.dataModule +import com.meloda.app.fast.friends.di.friendsModule +import com.meloda.app.fast.languagepicker.di.languagePickerModule +import com.meloda.app.fast.messageshistory.di.messagesHistoryModule +import com.meloda.app.fast.photoviewer.di.photoViewModule +import com.meloda.app.fast.profile.di.profileModule +import com.meloda.app.fast.service.longpolling.di.longPollModule +import com.meloda.app.fast.settings.di.settingsModule +import org.koin.android.ext.koin.androidContext +import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.core.module.dsl.singleOf +import org.koin.core.qualifier.qualifier +import org.koin.dsl.module + +val applicationModule = module { + includes(dataModule) + includes( + authModule, + conversationsModule, + settingsModule, + messagesHistoryModule, + photoViewModule, + languagePickerModule, + longPollModule, + friendsModule, + profileModule + ) + + // TODO: 14/05/2024, Danil Nikolaev: research on memory leaks and potentials errors + // TODO: 14/05/2024, Danil Nikolaev: extract all operations with preferences to standalone class + singleOf(PreferenceManager::getDefaultSharedPreferences) + single { androidContext().resources } + factory { androidContext().getSystemService(Context.POWER_SERVICE) as PowerManager } + + viewModelOf(::MainViewModelImpl) { + qualifier = qualifier("main") + } +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/model/LongPollState.kt b/app/src/main/kotlin/com/meloda/app/fast/model/LongPollState.kt new file mode 100644 index 00000000..6ef44939 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/model/LongPollState.kt @@ -0,0 +1,7 @@ +package com.meloda.app.fast.model + +sealed class LongPollState { + data object ForegroundService : LongPollState() + data object DefaultService : LongPollState() + data object Stop : LongPollState() +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/model/MainScreenState.kt b/app/src/main/kotlin/com/meloda/app/fast/model/MainScreenState.kt new file mode 100644 index 00000000..302a100d --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/model/MainScreenState.kt @@ -0,0 +1,30 @@ +package com.meloda.app.fast.model + +import androidx.compose.runtime.Immutable +import com.meloda.app.fast.model.database.AccountEntity + +@Immutable +data class MainScreenState( + val accounts: List, + val accountsLoaded: Boolean, + val useDarkTheme: Boolean, + val useDynamicColors: Boolean, + val isNeedToRequestNotifications: Boolean, + val isNeedToOpenAppPermissions: Boolean, + val isNeedToOpenAuth: Boolean, +) { + + companion object { + val EMPTY: MainScreenState = MainScreenState( + accounts = emptyList(), + accountsLoaded = false, + + // TODO: 05/05/2024, Danil Nikolaev: implement + useDarkTheme = false, + useDynamicColors = false, + isNeedToRequestNotifications = false, + isNeedToOpenAppPermissions = false, + isNeedToOpenAuth = false, + ) + } +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/model/ServicesState.kt b/app/src/main/kotlin/com/meloda/app/fast/model/ServicesState.kt new file mode 100644 index 00000000..664bf563 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/model/ServicesState.kt @@ -0,0 +1,7 @@ +package com.meloda.app.fast.model + +sealed class ServicesState { + data object Started : ServicesState() + data object Stopped : ServicesState() + data object Unknown : ServicesState() +} diff --git a/app/src/main/kotlin/com/meloda/fast/receiver/DownloadManagerReceiver.kt b/app/src/main/kotlin/com/meloda/app/fast/receiver/DownloadManagerReceiver.kt similarity index 89% rename from app/src/main/kotlin/com/meloda/fast/receiver/DownloadManagerReceiver.kt rename to app/src/main/kotlin/com/meloda/app/fast/receiver/DownloadManagerReceiver.kt index 5a80ffda..7b514d11 100644 --- a/app/src/main/kotlin/com/meloda/fast/receiver/DownloadManagerReceiver.kt +++ b/app/src/main/kotlin/com/meloda/app/fast/receiver/DownloadManagerReceiver.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.receiver +package com.meloda.app.fast.receiver import android.content.BroadcastReceiver import android.content.Context @@ -12,4 +12,4 @@ class DownloadManagerReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { onReceiveAction?.invoke() } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/service/OnlineService.kt b/app/src/main/kotlin/com/meloda/app/fast/service/OnlineService.kt new file mode 100644 index 00000000..120e03ff --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/service/OnlineService.kt @@ -0,0 +1,129 @@ +package com.meloda.app.fast.service + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import android.util.Log +import com.meloda.app.fast.common.UserConfig +import com.meloda.app.fast.common.extensions.createTimerFlow +import com.meloda.app.fast.data.api.account.AccountUseCase +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject +import kotlin.coroutines.CoroutineContext +import kotlin.time.Duration.Companion.minutes + +class OnlineService : Service() { + + private val job = SupervisorJob() + + private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + Log.d(TAG, "error: $throwable") + throwable.printStackTrace() + } + + private val coroutineContext: CoroutineContext + get() = Dispatchers.Default + job + exceptionHandler + + private val coroutineScope = CoroutineScope(coroutineContext) + + private val useCase: AccountUseCase by inject() + + private var timerJob: Job? = null + private var onlineJob: Job? = null + + override fun onBind(intent: Intent?): IBinder? { + Log.d(STATE_TAG, "onBind: intent: $intent") + return null + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (startId > 1) return START_STICKY + + Log.d(STATE_TAG, "onStartCommand: flags: $flags; startId: $startId\ninstance: $this") + + // TODO: 05/05/2024, Danil Nikolaev: implement +// if (AppGlobal.preferences.getBoolean( +// SettingsKeys.KEY_VISIBILITY_SEND_ONLINE_STATUS, +// SettingsKeys.DEFAULT_VALUE_KEY_VISIBILITY_SEND_ONLINE_STATUS +// ) +// ) { +// createTimer() +// } + + return START_STICKY + } + + private fun createTimer() { + timerJob = createTimerFlow( + isNeedToEndCondition = { false }, + onStartAction = ::setOnline, + onTickAction = ::setOnline, + interval = 5.minutes + ).launchIn(coroutineScope) + } + + private fun setOnline() { + if (onlineJob != null) return + + + // TODO: 05/05/2024, Danil Nikolaev: implement +// if (!AppGlobal.preferences.getBoolean( +// SettingsKeys.KEY_VISIBILITY_SEND_ONLINE_STATUS, +// SettingsKeys.DEFAULT_VALUE_KEY_VISIBILITY_SEND_ONLINE_STATUS +// ) +// ) return + + Log.d(TAG, "setOnline()") + + onlineJob = coroutineScope.launch { + val token = UserConfig.fastToken ?: UserConfig.accessToken + + if (token.isBlank()) { + Log.d(TAG, "setOnline: token is empty") + return@launch + } + + val response = useCase.setOnline( + voip = false, + accessToken = token + ) + Log.d(TAG, "setOnline: response: $response") + }.also { coroutine -> coroutine.invokeOnCompletion { onlineJob = null } } + } + + private suspend fun setOffline() { + Log.d(TAG, "setOffline()") + + val response = useCase.setOffline( + accessToken = UserConfig.accessToken + ) + + Log.d(TAG, "setOffline: response: $response") + } + + override fun onLowMemory() { + Log.d(STATE_TAG, "onLowMemory") + super.onLowMemory() + } + + override fun onDestroy() { + Log.d(STATE_TAG, "onDestroy") + + timerJob?.cancel("OnlineService destroyed") + onlineJob?.cancel("OnlineService destroyed") + + super.onDestroy() + } + + companion object { + private const val TAG = "OnlineService" + private const val STATE_TAG = "OnlineServiceState" + } +} diff --git a/app/src/main/kotlin/com/meloda/app/fast/service/longpolling/LongPollingService.kt b/app/src/main/kotlin/com/meloda/app/fast/service/longpolling/LongPollingService.kt new file mode 100644 index 00000000..3cb12fb2 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/service/longpolling/LongPollingService.kt @@ -0,0 +1,271 @@ +package com.meloda.app.fast.service.longpolling + +import android.app.PendingIntent +import android.app.Service +import android.content.Intent +import android.content.SharedPreferences +import android.net.Uri +import android.os.Build +import android.os.IBinder +import android.provider.Settings +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.ServiceCompat +import com.conena.nanokt.android.app.stopForegroundCompat +import com.meloda.app.fast.common.UserConfig +import com.meloda.app.fast.common.VkConstants +import com.meloda.app.fast.common.extensions.listenValue +import com.meloda.app.fast.data.LongPollUpdatesParser +import com.meloda.app.fast.data.LongPollUseCase +import com.meloda.app.fast.data.processState +import com.meloda.app.fast.datastore.SettingsController +import com.meloda.app.fast.datastore.SettingsKeys +import com.meloda.app.fast.model.api.data.LongPollUpdates +import com.meloda.app.fast.model.api.data.VkLongPollData +import com.meloda.app.fast.util.NotificationsUtils +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class LongPollingService : Service() { + + private val job = SupervisorJob() + + private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + Log.e(TAG, "error: $throwable") + + if (throwable !is NoAccessTokenException) { + throwable.printStackTrace() + } + } + + private val coroutineContext: CoroutineContext + get() = Dispatchers.IO + job + exceptionHandler + + private val coroutineScope = CoroutineScope(coroutineContext) + + private val longPollUseCase: LongPollUseCase by inject() + private val updatesParser: LongPollUpdatesParser by inject() + private val preferences: SharedPreferences by inject() + + private var currentJob: Job? = null + + override fun onCreate() { + super.onCreate() + Log.d(STATE_TAG, "onCreate()") + } + + override fun onBind(intent: Intent?): IBinder? { + Log.d(STATE_TAG, "onBind: intent: $intent") + return null + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (startId > 1) return START_STICKY + + val asForeground = preferences.getBoolean( + SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, + SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND + ) + + Log.d( + STATE_TAG, + "onStartCommand: asForeground: $asForeground; flags: $flags; startId: $startId;\ninstance: $this" + ) + + if (currentJob != null) { + currentJob?.cancel() + currentJob = null + } + + coroutineScope.launch { + currentJob = startPolling().also { it.join() } + } + + val openCategorySettingsIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) + .putExtra(Settings.EXTRA_APP_PACKAGE, packageName) + .putExtra(Settings.EXTRA_CHANNEL_ID, "long_polling") + } else { + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.fromParts("package", packageName, null)) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + val openCategorySettingsPendingIntent = PendingIntent.getActivity( + this, + 1, + openCategorySettingsIntent, + PendingIntent.FLAG_IMMUTABLE + ) + + if (asForeground) { + val notification = + NotificationsUtils.createNotification( + context = this, + title = "LongPoll", + contentText = "нажмите, чтобы убрать уведомление", + notRemovable = false, + channelId = "long_polling", + priority = NotificationsUtils.NotificationPriority.Low, + category = NotificationCompat.CATEGORY_SERVICE, + customNotificationId = NOTIFICATION_ID, + contentIntent = openCategorySettingsPendingIntent + ).build() + + startForeground(NOTIFICATION_ID, notification) + } else { + stopForegroundCompat(ServiceCompat.STOP_FOREGROUND_REMOVE) + } + return START_STICKY + } + + private fun startPolling(): Job { + if (job.isCompleted || job.isCancelled) { + Log.d(STATE_TAG, "job is completed or cancelled") + throw Exception("Job is over") + } + + Log.d(STATE_TAG, "job started") + + return coroutineScope.launch { + if (UserConfig.accessToken.isEmpty()) { + throw NoAccessTokenException + } + + var serverInfo = getServerInfo() + ?: throw LongPollException(message = "bad VK response (server info)") + + var lastUpdatesResponse: LongPollUpdates? = getUpdatesResponse(serverInfo) + ?: throw LongPollException(message = "initiation error: bad VK response (last updates)") + + var failCount = 0 + + while (job.isActive) { + if (lastUpdatesResponse == null) { + failCount++ + serverInfo = getServerInfo() + ?: throw LongPollException(message = "failed retrieving server info after error: bad VK response (server info #2)") + lastUpdatesResponse = getUpdatesResponse(serverInfo) + continue + } + + when (lastUpdatesResponse.failed) { + 1 -> { + val newTs = lastUpdatesResponse.ts ?: kotlin.run { + failCount++ + serverInfo.ts + } + + lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs)) + } + + 2, 3 -> { + serverInfo = getServerInfo() + ?: throw LongPollException( + message = "failed retrieving server info after error: bad VK response (server info #3)" + ) + lastUpdatesResponse = getUpdatesResponse(serverInfo) + } + + else -> { + val newTs = lastUpdatesResponse.ts + + if (newTs == null) { + failCount++ + } else { + val updates = lastUpdatesResponse.updates + + if (updates == null) { + failCount++ + } else { + updates.forEach(updatesParser::parseNextUpdate) + } + + lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs)) + } + } + } + } + } + } + + private suspend fun getServerInfo(): VkLongPollData? = suspendCoroutine { + longPollUseCase.getLongPollServer( + needPts = true, + version = VkConstants.LP_VERSION + ).listenValue(coroutineScope) { state -> + state.processState( + success = { response -> + Log.d(TAG, "getServerInfo: serverInfoResponse: $response") + it.resume(response) + }, + error = { error -> + Log.e(TAG, "getServerInfo: $error") + it.resume(null) + } + ) + } + } + + private suspend fun getUpdatesResponse( + server: VkLongPollData + ): LongPollUpdates? = suspendCoroutine { + longPollUseCase.getLongPollUpdates( + serverUrl = "https://${server.server}", + key = server.key, + ts = server.ts, + wait = 25, + mode = 2 or 8 or 32 or 64 or 128, + version = VkConstants.LP_VERSION + ).listenValue(coroutineScope) { state -> + state.processState( + success = { response -> + Log.d(TAG, "lastUpdateResponse: $response") + it.resume(response) + }, + error = { error -> + Log.d(TAG, "getUpdatesResponse: error: $error") + it.resume(null) + } + ) + } + } + + override fun onDestroy() { + Log.d(STATE_TAG, "onDestroy") + try { + SettingsController.edit { + putBoolean(KEY_LONG_POLL_WAS_DESTROYED, true) + } + job.cancel() + } catch (e: Exception) { + e.printStackTrace() + } + super.onDestroy() + } + + override fun onLowMemory() { + Log.d(STATE_TAG, "onLowMemory") + super.onLowMemory() + } + + companion object { + const val TAG = "LongPollTask" + + private const val STATE_TAG = "LongPollServiceState" + + const val KEY_LONG_POLL_WAS_DESTROYED = "long_poll_was_destroyed" + + private const val NOTIFICATION_ID = 1001 + } +} + +private data class LongPollException(override val message: String) : Throwable() +private data object NoAccessTokenException : Throwable() diff --git a/app/src/main/kotlin/com/meloda/app/fast/service/longpolling/di/LongPollModule.kt b/app/src/main/kotlin/com/meloda/app/fast/service/longpolling/di/LongPollModule.kt new file mode 100644 index 00000000..68a42c2f --- /dev/null +++ b/app/src/main/kotlin/com/meloda/app/fast/service/longpolling/di/LongPollModule.kt @@ -0,0 +1,13 @@ +package com.meloda.app.fast.service.longpolling.di + +import com.meloda.app.fast.data.LongPollUpdatesParser +import com.meloda.app.fast.data.LongPollUseCase +import com.meloda.app.fast.data.LongPollUseCaseImpl +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.bind +import org.koin.dsl.module + +val longPollModule = module { + singleOf(::LongPollUseCaseImpl) bind LongPollUseCase::class + singleOf(::LongPollUpdatesParser) +} diff --git a/app/src/main/kotlin/com/meloda/fast/util/NotificationsUtils.kt b/app/src/main/kotlin/com/meloda/app/fast/util/NotificationsUtils.kt similarity index 93% rename from app/src/main/kotlin/com/meloda/fast/util/NotificationsUtils.kt rename to app/src/main/kotlin/com/meloda/app/fast/util/NotificationsUtils.kt index efb8294f..523bc569 100644 --- a/app/src/main/kotlin/com/meloda/fast/util/NotificationsUtils.kt +++ b/app/src/main/kotlin/com/meloda/app/fast/util/NotificationsUtils.kt @@ -1,11 +1,11 @@ -package com.meloda.fast.util +package com.meloda.app.fast.util import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import com.meloda.fast.R +import com.meloda.app.fast.designsystem.R as UiR object NotificationsUtils { @@ -27,7 +27,7 @@ object NotificationsUtils { actions: List = emptyList(), ): NotificationCompat.Builder { val builder = NotificationCompat.Builder(context, channelId) - .setSmallIcon(R.drawable.ic_fast_logo) + .setSmallIcon(UiR.drawable.ic_fast_logo) .setContentTitle(title) .setPriority(priority.value) .setContentIntent(contentIntent) @@ -69,5 +69,4 @@ object NotificationsUtils { enum class NotificationPriority(val value: Int) { Default(0), Low(-1), Min(-2), High(1), Max(2) } - } diff --git a/app/src/main/kotlin/com/meloda/fast/api/ApiEvent.kt b/app/src/main/kotlin/com/meloda/fast/api/ApiEvent.kt deleted file mode 100644 index bbdad63b..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/ApiEvent.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.api - -enum class ApiEvent(val value: Int) { - MessageSetFlags(2), - MessageClearFlags(3), - MessageNew(4), - MessageEdit(5), - MessageReadIncoming(6), - MessageReadOutgoing(7), - MessagesDeleted(13), - PinUnpinConversation(20), - PrivateTyping(61), - ChatTyping(62), - OneMoreTyping(63), - VoiceRecording(64), - PhotoUploading(65), - VideoUploading(66), - FileUploading(67), - UnreadCountUpdate(80) - ; - - companion object { - fun parse(value: Int) = values().firstOrNull { it.value == value } - } - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/ApiExtensions.kt b/app/src/main/kotlin/com/meloda/fast/api/ApiExtensions.kt deleted file mode 100644 index 83a8ee8c..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/ApiExtensions.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.meloda.fast.api - -object ApiExtensions { - - val Boolean.intString get() = (if (this) 1 else 0).toString() - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt b/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt deleted file mode 100644 index d55a408c..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt +++ /dev/null @@ -1,1285 +0,0 @@ -package com.meloda.fast.api - -import android.content.Context -import android.graphics.Typeface -import android.graphics.drawable.Drawable -import android.text.SpannableString -import android.text.SpannableStringBuilder -import android.text.Spanned -import android.text.TextPaint -import android.text.style.ClickableSpan -import android.text.style.StyleSpan -import android.view.View -import androidx.core.content.ContextCompat -import com.google.gson.Gson -import com.meloda.fast.R -import com.meloda.fast.api.base.ApiError -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.model.attachments.* -import com.meloda.fast.api.model.base.BaseVkMessage -import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem -import com.meloda.fast.api.model.domain.VkConversationDomain -import com.meloda.fast.api.network.* -import com.meloda.fast.ext.orDots -import com.meloda.fast.model.base.UiImage -import com.meloda.fast.model.base.UiText - -@Suppress("MemberVisibilityCanBePrivate") -object VkUtils { - - fun attachmentToString( - attachmentClass: Class, - id: Int, - ownerId: Int, - withAccessKey: Boolean, - accessKey: String?, - ): String { - val type = when (attachmentClass) { - VkAudio::class.java -> "audio" - VkFile::class.java -> "doc" - VkVideo::class.java -> "video" - VkPhoto::class.java -> "photo" - VkWall::class.java -> "wall" - else -> throw IllegalArgumentException("unknown attachment class: $attachmentClass") - } - - val result = StringBuilder(type).append(ownerId).append('_').append(id) - if (withAccessKey && !accessKey.isNullOrBlank()) { - result.append('_') - result.append(accessKey) - } - return result.toString() - } - - - fun getMessageUser(message: VkMessage, profiles: Map): VkUser? { - return (if (!message.isUser()) null - else profiles[message.fromId]).also { message.user = it } - } - - fun getMessageActionUser(message: VkMessage, profiles: Map): VkUser? { - return if (message.actionMemberId == null || message.actionMemberId <= 0) null - else profiles[message.actionMemberId] - } - - fun getMessageGroup(message: VkMessage, groups: Map): VkGroup? { - return (if (!message.isGroup()) null - else groups[message.fromId]).also { message.group = it } - } - - fun getMessageActionGroup(message: VkMessage, groups: Map): VkGroup? { - return if (message.actionMemberId == null || message.actionMemberId >= 0) null - else groups[message.actionMemberId] - } - - fun getMessageAvatar( - message: VkMessage, - messageUser: VkUser?, - messageGroup: VkGroup?, - ): String? { - return when { - message.isUser() -> messageUser?.photo200 - message.isGroup() -> messageGroup?.photo200 - else -> null - } - } - - fun getMessageTitle( - message: VkMessage, - defMessageUser: VkUser? = null, - defMessageGroup: VkGroup? = null, - profiles: Map? = null, - groups: Map? = null, - ): String? { - val messageUser: VkUser? = - defMessageUser ?: if (profiles == null) null - else profiles[message.fromId] - - val messageGroup: VkGroup? = - defMessageGroup ?: if (groups == null) null - else groups[message.fromId] - - return when { - message.isUser() -> messageUser?.fullName - message.isGroup() -> messageGroup?.name - else -> null - } - } - - fun getConversationUser( - conversation: VkConversationDomain, - profiles: Map - ): VkUser? { - return if (!conversation.isUser()) null - else profiles[conversation.id] - } - - fun getConversationGroup( - conversation: VkConversationDomain, - groups: Map - ): VkGroup? { - return if (!conversation.isGroup()) null - else groups[conversation.id] - } - - fun getConversationAvatar( - conversation: VkConversationDomain, - conversationUser: VkUser?, - conversationGroup: VkGroup?, - ): String? { - return when { - conversation.isAccount() -> null - conversation.isUser() -> conversationUser?.photo200 - conversation.isGroup() -> conversationGroup?.photo200 - conversation.isChat() -> conversation.conversationPhoto - else -> null - } - } - - fun getConversationTitle( - context: Context, - conversation: VkConversationDomain, - defConversationUser: VkUser? = null, - defConversationGroup: VkGroup? = null, - profiles: Map? = null, - groups: Map? = null, - ): String? { - val conversationUser: VkUser? = - defConversationUser ?: if (profiles == null) null - else getConversationUser(conversation, profiles) - - val conversationGroup: VkGroup? = - defConversationGroup ?: if (groups == null) null - else getConversationGroup(conversation, groups) - - return when { - conversation.isAccount() -> context.getString(R.string.favorites) - conversation.isChat() -> conversation.conversationTitle - conversation.isUser() -> conversationUser?.fullName - conversation.isGroup() -> conversationGroup?.name - else -> null - } - } - - fun getConversationUserGroup( - conversation: VkConversationDomain, - profiles: Map, - groups: Map, - ): Pair { - val user: VkUser? = getConversationUser(conversation, profiles) - val group: VkGroup? = getConversationGroup(conversation, groups) - - return user to group - } - - fun getMessageUserGroup( - message: VkMessage?, - profiles: Map, - groups: Map, - ): Pair { - if (message == null) return null to null - - val user: VkUser? = getMessageUser(message, profiles) - val group: VkGroup? = getMessageGroup(message, groups) - - return user to group - } - - fun getMessageActionUserGroup( - message: VkMessage?, - profiles: Map, - groups: Map, - ): Pair { - if (message == null) return null to null - - val user: VkUser? = getMessageActionUser(message, profiles) - val group: VkGroup? = getMessageActionGroup(message, groups) - - return user to group - } - - fun prepareMessageText(text: String, forConversations: Boolean = false): String { - return text.apply { - if (forConversations) { - replace("\n", "") - } - - replace("&", "&") - replace(""", "\"") - replace("
", "\n") - replace(">", ">") - replace("<", "<") - replace("
", "\n") - replace("–", "-") - trim() - } - } - - fun isPreviousMessageSentFiveMinutesAgo(prevMessage: VkMessage?, message: VkMessage?) = - prevMessage != null && message != null && (message.date - prevMessage.date >= 300) - - fun isPreviousMessageFromDifferentSender(prevMessage: VkMessage?, message: VkMessage?) = - prevMessage != null && message != null && prevMessage.fromId != message.fromId - - fun parseForwards(baseForwards: List?): List? { - if (baseForwards.isNullOrEmpty()) return null - - val forwards = mutableListOf() - - for (baseForward in baseForwards) { - forwards += baseForward.asVkMessage() - } - - return forwards - } - - fun parseReplyMessage(baseReplyMessage: BaseVkMessage?): VkMessage? { - if (baseReplyMessage == null) return null - - return baseReplyMessage.asVkMessage() - } - - fun parseAttachments(baseAttachments: List?): List? { - if (baseAttachments.isNullOrEmpty()) return null - - val attachments = mutableListOf() - - for (baseAttachment in baseAttachments) { - when (baseAttachment.getPreparedType()) { - BaseVkAttachmentItem.AttachmentType.Photo -> { - val photo = baseAttachment.photo ?: continue - attachments += photo.asVkPhoto() - } - - BaseVkAttachmentItem.AttachmentType.Video -> { - val video = baseAttachment.video ?: continue - attachments += video.asVkVideo() - } - - BaseVkAttachmentItem.AttachmentType.Audio -> { - val audio = baseAttachment.audio ?: continue - attachments += audio.asVkAudio() - } - - BaseVkAttachmentItem.AttachmentType.File -> { - val file = baseAttachment.file ?: continue - attachments += file.asVkFile() - } - - BaseVkAttachmentItem.AttachmentType.Link -> { - val link = baseAttachment.link ?: continue - attachments += link.asVkLink() - } - - BaseVkAttachmentItem.AttachmentType.MiniApp -> { - val miniApp = baseAttachment.miniApp ?: continue - attachments += miniApp.asVkMiniApp() - } - - BaseVkAttachmentItem.AttachmentType.Voice -> { - val voiceMessage = baseAttachment.voiceMessage ?: continue - attachments += voiceMessage.asVkVoiceMessage() - } - - BaseVkAttachmentItem.AttachmentType.Sticker -> { - val sticker = baseAttachment.sticker ?: continue - attachments += sticker.asVkSticker() - } - - BaseVkAttachmentItem.AttachmentType.Gift -> { - val gift = baseAttachment.gift ?: continue - attachments += gift.asVkGift() - } - - BaseVkAttachmentItem.AttachmentType.Wall -> { - val wall = baseAttachment.wall ?: continue - attachments += wall.asVkWall() - } - - BaseVkAttachmentItem.AttachmentType.Graffiti -> { - val graffiti = baseAttachment.graffiti ?: continue - attachments += graffiti.asVkGraffiti() - } - - BaseVkAttachmentItem.AttachmentType.Poll -> { - val poll = baseAttachment.poll ?: continue - attachments += poll.asVkPoll() - } - - BaseVkAttachmentItem.AttachmentType.WallReply -> { - val wallReply = baseAttachment.wallReply ?: continue - attachments += wallReply.asVkWallReply() - } - - BaseVkAttachmentItem.AttachmentType.Call -> { - val call = baseAttachment.call ?: continue - attachments += call.asVkCall() - } - - BaseVkAttachmentItem.AttachmentType.GroupCallInProgress -> { - val groupCall = baseAttachment.groupCall ?: continue - attachments += groupCall.asVkGroupCall() - } - - BaseVkAttachmentItem.AttachmentType.Curator -> { - val curator = baseAttachment.curator ?: continue - attachments += curator.asVkCurator() - } - - BaseVkAttachmentItem.AttachmentType.Event -> { - val event = baseAttachment.event ?: continue - attachments += event.asVkEvent() - } - - BaseVkAttachmentItem.AttachmentType.Story -> { - val story = baseAttachment.story ?: continue - attachments += story.asVkStory() - } - - BaseVkAttachmentItem.AttachmentType.Widget -> { - val widget = baseAttachment.widget ?: continue - attachments += widget.asVkWidget() - } - - else -> continue - } - } - - return attachments - } - - fun getActionMessageText( - message: VkMessage?, - youPrefix: String, - messageUser: VkUser?, - messageGroup: VkGroup?, - action: VkMessage.Action?, - actionUser: VkUser?, - actionGroup: VkGroup?, - ): UiText? { - if (message == null) return null - - return when (action) { - VkMessage.Action.CHAT_CREATE -> { - val text = message.actionText ?: return null - - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams( - R.string.message_action_chat_created, - listOf(prefix, text) - ) - } - - VkMessage.Action.CHAT_TITLE_UPDATE -> { - val text = message.actionText ?: return null - - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams( - R.string.message_action_chat_renamed, - listOf(prefix, text) - ) - } - - VkMessage.Action.CHAT_PHOTO_UPDATE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams(R.string.message_action_chat_photo_update, listOf(prefix)) - } - - VkMessage.Action.CHAT_PHOTO_REMOVE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams(R.string.message_action_chat_photo_remove, listOf(prefix)) - } - - VkMessage.Action.CHAT_KICK_USER -> { - val memberId = message.actionMemberId ?: return null - val isUser = memberId > 0 - val isGroup = memberId < 0 - - if (isUser && actionUser == null) return null - if (isGroup && actionGroup == null) return null - - if (memberId == message.fromId) { - val prefix = if (memberId == UserConfig.userId) youPrefix - else actionUser.toString() - - UiText.ResourceParams(R.string.message_action_chat_user_left, listOf(prefix)) - } else { - val prefix = - if (message.fromId == UserConfig.userId) youPrefix - else messageUser?.toString() ?: messageGroup?.toString().orDots() - - val postfix = - if (memberId == UserConfig.userId) youPrefix.lowercase() - else actionUser.toString() - - UiText.ResourceParams( - R.string.message_action_chat_user_kicked, listOf(prefix, postfix) - ) - } - } - - VkMessage.Action.CHAT_INVITE_USER -> { - val memberId = message.actionMemberId ?: 0 - val isUser = memberId > 0 - val isGroup = memberId < 0 - - if (isUser && actionUser == null) return null - if (isGroup && actionGroup == null) return null - - if (memberId == message.fromId) { - val prefix = if (memberId == UserConfig.userId) youPrefix - else actionUser.toString() - - UiText.ResourceParams( - R.string.message_action_chat_user_returned, - listOf(prefix) - ) - } else { - val prefix = if (message.fromId == UserConfig.userId) youPrefix - else messageUser?.toString() ?: messageGroup?.toString().orDots() - - val postfix = - if (memberId == UserConfig.userId) youPrefix.lowercase() - else actionUser.toString() - - UiText.ResourceParams( - R.string.message_action_chat_user_invited, - listOf(prefix, postfix) - ) - } - } - - VkMessage.Action.CHAT_INVITE_USER_BY_LINK -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams( - R.string.message_action_chat_user_joined_by_link, - listOf(prefix) - ) - } - - VkMessage.Action.CHAT_INVITE_USER_BY_CALL -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams( - R.string.message_action_chat_user_joined_by_call, - listOf(prefix) - ) - } - - VkMessage.Action.CHAT_INVITE_USER_BY_CALL_LINK -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams( - R.string.message_action_chat_user_joined_by_call_link, - listOf(prefix) - ) - } - - VkMessage.Action.CHAT_PIN_MESSAGE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams(R.string.message_action_chat_pin_message, listOf(prefix)) - } - - VkMessage.Action.CHAT_UNPIN_MESSAGE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams(R.string.message_action_chat_unpin_message, listOf(prefix)) - } - - VkMessage.Action.CHAT_SCREENSHOT -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams(R.string.message_action_chat_screenshot, listOf(prefix)) - } - - VkMessage.Action.CHAT_STYLE_UPDATE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - UiText.ResourceParams(R.string.message_action_chat_style_update, listOf(prefix)) - } - - null -> null - } - } - - fun getActionMessageText( - context: Context, - message: VkMessage?, - youPrefix: String, - messageUser: VkUser?, - messageGroup: VkGroup?, - action: VkMessage.Action?, - actionUser: VkUser?, - actionGroup: VkGroup?, - ): SpannableString? { - if (message == null) return null - - return when (action) { - VkMessage.Action.CHAT_CREATE -> { - val text = message.actionText ?: return null - - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_created, prefix, text) - - val startIndex = spanText.indexOf(text, startIndex = prefix.length) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - it.setSpan( - StyleSpan(Typeface.BOLD), - startIndex, - startIndex + text.length, 0 - ) - } - } - - VkMessage.Action.CHAT_TITLE_UPDATE -> { - val text = message.actionText ?: return null - - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_renamed, prefix, text) - val startIndex = spanText.indexOf(text) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - it.setSpan( - StyleSpan(Typeface.BOLD), startIndex, startIndex + text.length, 0 - ) - } - } - - VkMessage.Action.CHAT_PHOTO_UPDATE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_photo_update, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } - - VkMessage.Action.CHAT_PHOTO_REMOVE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_photo_remove, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } - - VkMessage.Action.CHAT_KICK_USER -> { - val memberId = message.actionMemberId ?: return null - val isUser = memberId > 0 - val isGroup = memberId < 0 - - if (isUser && actionUser == null) return null - if (isGroup && actionGroup == null) return null - - if (memberId == message.fromId) { - val prefix = if (memberId == UserConfig.userId) youPrefix - else actionUser.toString() - - val spanText = - context.getString(R.string.message_action_chat_user_left, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } else { - val prefix = - if (message.fromId == UserConfig.userId) youPrefix - else messageUser?.toString() ?: messageGroup?.toString().orDots() - - val postfix = - if (memberId == UserConfig.userId) youPrefix.lowercase() - else actionUser.toString() - - val spanText = - context.getString( - R.string.message_action_chat_user_kicked, - prefix, - postfix - ) - val startIndex = spanText.indexOf(postfix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - it.setSpan( - StyleSpan(Typeface.BOLD), startIndex, startIndex + postfix.length, 0 - ) - } - } - } - - VkMessage.Action.CHAT_INVITE_USER -> { - val memberId = message.actionMemberId ?: 0 - val isUser = memberId > 0 - val isGroup = memberId < 0 - - if (isUser && actionUser == null) return null - if (isGroup && actionGroup == null) return null - - if (memberId == message.fromId) { - val prefix = if (memberId == UserConfig.userId) youPrefix - else actionUser.toString() - - val spanText = - context.getString(R.string.message_action_chat_user_returned, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } else { - val prefix = if (message.fromId == UserConfig.userId) youPrefix - else messageUser?.toString() ?: messageGroup?.toString().orDots() - - val postfix = - if (memberId == UserConfig.userId) youPrefix.lowercase() - else actionUser.toString() - - val spanText = - context.getString( - R.string.message_action_chat_user_invited, - prefix, - postfix - ) - val startIndex = spanText.indexOf(postfix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - it.setSpan( - StyleSpan(Typeface.BOLD), startIndex, startIndex + postfix.length, 0 - ) - } - } - } - - VkMessage.Action.CHAT_INVITE_USER_BY_LINK -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_user_joined_by_link, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } - - VkMessage.Action.CHAT_INVITE_USER_BY_CALL -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_user_joined_by_call, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } - - VkMessage.Action.CHAT_INVITE_USER_BY_CALL_LINK -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_user_joined_by_call_link, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } - - VkMessage.Action.CHAT_PIN_MESSAGE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_pin_message, prefix).trim() - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } - - VkMessage.Action.CHAT_UNPIN_MESSAGE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_unpin_message, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } - - VkMessage.Action.CHAT_SCREENSHOT -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isGroup() -> messageGroup?.name - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_screenshot, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } - - VkMessage.Action.CHAT_STYLE_UPDATE -> { - val prefix = when { - message.fromId == UserConfig.userId -> youPrefix - message.isUser() -> messageUser?.toString() - else -> return null - } ?: return null - - val spanText = - context.getString(R.string.message_action_chat_style_update, prefix) - - SpannableString(spanText).also { - it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) - } - } - - null -> null - } - } - - fun getActionConversationText( - message: VkMessage?, - youPrefix: String, - messageUser: VkUser? = null, - messageGroup: VkGroup? = null, - action: VkMessage.Action?, - actionUser: VkUser?, - actionGroup: VkGroup?, - ): UiText? { - return getActionMessageText( - message = message, - youPrefix = youPrefix, - messageUser = messageUser, - messageGroup = messageGroup, - action = action, - actionUser = actionUser, - actionGroup = actionGroup, - ) - } - - fun getActionConversationText( - context: Context, - message: VkMessage?, - youPrefix: String, - messageUser: VkUser? = null, - messageGroup: VkGroup? = null, - action: VkMessage.Action?, - actionUser: VkUser?, - actionGroup: VkGroup?, - ): String? { - return getActionMessageText( - context = context, - message = message, - youPrefix = youPrefix, - messageUser = messageUser, - messageGroup = messageGroup, - action = action, - actionUser = actionUser, - actionGroup = actionGroup, - )?.toString() - } - - fun getForwardsText(message: VkMessage?): UiText? { - if (message?.forwards.isNullOrEmpty()) return null - - return message?.forwards?.let { forwards -> - UiText.Resource( - if (forwards.size == 1) R.string.forwarded_message - else R.string.forwarded_messages - ) - } - } - - fun getAttachmentText(message: VkMessage?): UiText? { - message?.geo?.let { - return when (it.type) { - "point" -> UiText.Resource(R.string.message_geo_point) - else -> UiText.Resource(R.string.message_geo) - } - } - if (message?.attachments.isNullOrEmpty()) return null - - return message?.attachments?.let { attachments -> - if (attachments.size == 1) { - getAttachmentTypeByClass(attachments[0])?.let { - getAttachmentTextByType(it) - } - } else { - if (isAttachmentsHaveOneType(attachments)) { - getAttachmentTypeByClass(attachments[0])?.let { - getAttachmentTextByType(it, attachments.size) - } - } else { - UiText.Resource(R.string.message_attachments_many) - } - } - } - } - - fun getAttachmentConversationIcon(message: VkMessage?): UiImage? { - return message?.attachments?.let { attachments -> - if (attachments.isEmpty()) return null - if (attachments.size == 1 || isAttachmentsHaveOneType(attachments)) { - message.geo?.let { - return UiImage.Resource(R.drawable.ic_map_marker) - } - - getAttachmentTypeByClass(attachments[0])?.let { - getAttachmentIconByType(it) - } - } else { - UiImage.Resource(R.drawable.ic_baseline_attach_file_24) - } - } - } - - fun getAttachmentConversationIcon( - context: Context, - message: VkMessage?, - ): Drawable? { - if (message == null) return null - return message.attachments?.let { attachments -> - if (attachments.size == 1 || isAttachmentsHaveOneType(attachments)) { - message.geo?.let { - return ContextCompat.getDrawable(context, R.drawable.ic_map_marker) - } - - if (attachments.isEmpty()) return null - - getAttachmentTypeByClass(attachments[0])?.let { - getAttachmentIconByType( - context, - it - ) - } - } else { - ContextCompat.getDrawable(context, R.drawable.ic_baseline_attach_file_24) - } - } - } - - fun getAttachmentIconByType(attachmentType: BaseVkAttachmentItem.AttachmentType): UiImage? { - return when (attachmentType) { - BaseVkAttachmentItem.AttachmentType.Photo -> R.drawable.ic_attachment_photo - BaseVkAttachmentItem.AttachmentType.Video -> R.drawable.ic_attachment_video - BaseVkAttachmentItem.AttachmentType.Audio -> R.drawable.ic_attachment_audio - BaseVkAttachmentItem.AttachmentType.File -> R.drawable.ic_attachment_file - BaseVkAttachmentItem.AttachmentType.Link -> R.drawable.ic_attachment_link - BaseVkAttachmentItem.AttachmentType.Voice -> R.drawable.ic_attachment_voice - BaseVkAttachmentItem.AttachmentType.MiniApp -> R.drawable.ic_attachment_mini_app - BaseVkAttachmentItem.AttachmentType.Sticker -> R.drawable.ic_attachment_sticker - BaseVkAttachmentItem.AttachmentType.Gift -> R.drawable.ic_attachment_gift - BaseVkAttachmentItem.AttachmentType.Wall -> R.drawable.ic_attachment_wall - BaseVkAttachmentItem.AttachmentType.Graffiti -> R.drawable.ic_attachment_graffiti - BaseVkAttachmentItem.AttachmentType.Poll -> R.drawable.ic_attachment_poll - BaseVkAttachmentItem.AttachmentType.WallReply -> R.drawable.ic_attachment_wall_reply - BaseVkAttachmentItem.AttachmentType.Call -> R.drawable.ic_attachment_call - BaseVkAttachmentItem.AttachmentType.GroupCallInProgress -> R.drawable.ic_attachment_group_call - BaseVkAttachmentItem.AttachmentType.Story -> R.drawable.ic_attachment_story - else -> null - }?.let(UiImage::Resource) - } - - @Deprecated("Use new with UiImage") - fun getAttachmentIconByType( - context: Context, - attachmentType: BaseVkAttachmentItem.AttachmentType, - ): Drawable? { - val resId = when (attachmentType) { - BaseVkAttachmentItem.AttachmentType.Photo -> R.drawable.ic_attachment_photo - BaseVkAttachmentItem.AttachmentType.Video -> R.drawable.ic_attachment_video - BaseVkAttachmentItem.AttachmentType.Audio -> R.drawable.ic_attachment_audio - BaseVkAttachmentItem.AttachmentType.File -> R.drawable.ic_attachment_file - BaseVkAttachmentItem.AttachmentType.Link -> R.drawable.ic_attachment_link - BaseVkAttachmentItem.AttachmentType.Voice -> R.drawable.ic_attachment_voice - BaseVkAttachmentItem.AttachmentType.MiniApp -> R.drawable.ic_attachment_mini_app - BaseVkAttachmentItem.AttachmentType.Sticker -> R.drawable.ic_attachment_sticker - BaseVkAttachmentItem.AttachmentType.Gift -> R.drawable.ic_attachment_gift - BaseVkAttachmentItem.AttachmentType.Wall -> R.drawable.ic_attachment_wall - BaseVkAttachmentItem.AttachmentType.Graffiti -> R.drawable.ic_attachment_graffiti - BaseVkAttachmentItem.AttachmentType.Poll -> R.drawable.ic_attachment_poll - BaseVkAttachmentItem.AttachmentType.WallReply -> R.drawable.ic_attachment_wall_reply - BaseVkAttachmentItem.AttachmentType.Call -> R.drawable.ic_attachment_call - BaseVkAttachmentItem.AttachmentType.GroupCallInProgress -> R.drawable.ic_attachment_group_call - BaseVkAttachmentItem.AttachmentType.Story -> R.drawable.ic_attachment_story - else -> return null - } - - return ContextCompat.getDrawable(context, resId) - } - - fun isAttachmentsHaveOneType(attachments: List): Boolean { - if (attachments.isEmpty()) return true - if (attachments.size == 1) return true - - val firstType = getAttachmentTypeByClass(attachments[0]) - for (i in 1 until attachments.size) { - val type = getAttachmentTypeByClass(attachments[i]) - if (type != firstType) return false - } - - return true - } - - fun getAttachmentTypeByClass(attachment: VkAttachment): BaseVkAttachmentItem.AttachmentType? { - return when (attachment) { - is VkPhoto -> BaseVkAttachmentItem.AttachmentType.Photo - is VkVideo -> BaseVkAttachmentItem.AttachmentType.Video - is VkAudio -> BaseVkAttachmentItem.AttachmentType.Audio - is VkFile -> BaseVkAttachmentItem.AttachmentType.File - is VkLink -> BaseVkAttachmentItem.AttachmentType.Link - is VkMiniApp -> BaseVkAttachmentItem.AttachmentType.MiniApp - is VkVoiceMessage -> BaseVkAttachmentItem.AttachmentType.Voice - is VkSticker -> BaseVkAttachmentItem.AttachmentType.Sticker - is VkGift -> BaseVkAttachmentItem.AttachmentType.Gift - is VkWall -> BaseVkAttachmentItem.AttachmentType.Wall - is VkGraffiti -> BaseVkAttachmentItem.AttachmentType.Graffiti - is VkPoll -> BaseVkAttachmentItem.AttachmentType.Poll - is VkWallReply -> BaseVkAttachmentItem.AttachmentType.WallReply - is VkCall -> BaseVkAttachmentItem.AttachmentType.Call - is VkGroupCall -> BaseVkAttachmentItem.AttachmentType.GroupCallInProgress - is VkEvent -> BaseVkAttachmentItem.AttachmentType.Event - is VkCurator -> BaseVkAttachmentItem.AttachmentType.Curator - is VkStory -> BaseVkAttachmentItem.AttachmentType.Story - is VkWidget -> BaseVkAttachmentItem.AttachmentType.Widget - else -> null - } - } - - fun getAttachmentTextByType( - attachmentType: BaseVkAttachmentItem.AttachmentType, - size: Int = 1, - ): UiText { - return when (attachmentType) { - BaseVkAttachmentItem.AttachmentType.Photo -> - UiText.QuantityResource(R.plurals.attachment_photos, size) - - BaseVkAttachmentItem.AttachmentType.Video -> - UiText.QuantityResource(R.plurals.attachment_videos, size) - - BaseVkAttachmentItem.AttachmentType.Audio -> - UiText.QuantityResource(R.plurals.attachment_audios, size) - - BaseVkAttachmentItem.AttachmentType.File -> - UiText.QuantityResource(R.plurals.attachment_files, size) - - BaseVkAttachmentItem.AttachmentType.Link -> - UiText.Resource(R.string.message_attachments_link) - - BaseVkAttachmentItem.AttachmentType.Voice -> - UiText.Resource(R.string.message_attachments_voice) - - BaseVkAttachmentItem.AttachmentType.MiniApp -> - UiText.Resource(R.string.message_attachments_mini_app) - - BaseVkAttachmentItem.AttachmentType.Sticker -> - UiText.Resource(R.string.message_attachments_sticker) - - BaseVkAttachmentItem.AttachmentType.Gift -> - UiText.Resource(R.string.message_attachments_gift) - - BaseVkAttachmentItem.AttachmentType.Wall -> - UiText.Resource(R.string.message_attachments_wall) - - BaseVkAttachmentItem.AttachmentType.Graffiti -> - UiText.Resource(R.string.message_attachments_graffiti) - - BaseVkAttachmentItem.AttachmentType.Poll -> - UiText.Resource(R.string.message_attachments_poll) - - BaseVkAttachmentItem.AttachmentType.WallReply -> - UiText.Resource(R.string.message_attachments_wall_reply) - - BaseVkAttachmentItem.AttachmentType.Call -> - UiText.Resource(R.string.message_attachments_call) - - BaseVkAttachmentItem.AttachmentType.GroupCallInProgress -> - UiText.Resource(R.string.message_attachments_call_in_progress) - - BaseVkAttachmentItem.AttachmentType.Event -> - UiText.Resource(R.string.message_attachments_event) - - BaseVkAttachmentItem.AttachmentType.Curator -> - UiText.Resource(R.string.message_attachments_curator) - - BaseVkAttachmentItem.AttachmentType.Story -> - UiText.Resource(R.string.message_attachments_story) - - BaseVkAttachmentItem.AttachmentType.Widget -> - UiText.Resource(R.string.message_attachments_widget) - - else -> UiText.Simple(attachmentType.value) - } - } - - fun getApiError(gson: Gson, errorString: String?): ApiAnswer.Error { - try { - val defaultError = gson.fromJson(errorString, ApiError::class.java) - - val error: ApiError = - when (defaultError.error) { - VkErrorCodes.UserAuthorizationFailed.toString() -> { - val authorizationError = - gson.fromJson(errorString, AuthorizationError::class.java) - - authorizationError - } - - VkErrorCodes.AccessTokenExpired.toString() -> { - val tokenExpiredError = - gson.fromJson(errorString, TokenExpiredError::class.java) - - tokenExpiredError - } - - VkErrors.NeedValidation -> { - val validationError = - gson.fromJson( - errorString, - if (defaultError.errorMessage == VkErrorMessages.UserBanned) { - UserBannedError::class.java - } else { - ValidationRequiredError::class.java - } - ) - - validationError - } - - VkErrors.NeedCaptcha -> { - val captchaRequiredError = - gson.fromJson(errorString, CaptchaRequiredError::class.java) - - captchaRequiredError - } - - VkErrors.InvalidRequest -> { - when (defaultError.errorType) { - VkErrorTypes.OtpFormatIncorrect -> WrongTwoFaCodeFormatError - VkErrorTypes.WrongOtp -> WrongTwoFaCodeError - else -> defaultError - } - } - - else -> defaultError - } - - return ApiAnswer.Error(error) - } catch (e: Exception) { - return ApiAnswer.Error(ApiError(throwable = e)) - } - } - - fun visualizeMentions( - messageText: String, - mentionColor: Int, - onMentionClick: ((id: Int) -> Unit)? = null, - ): SpannableStringBuilder { - if (messageText.isEmpty()) { - return SpannableStringBuilder("") - } - - var newMessageText = messageText - - val idsIndexes = mutableListOf>() - val mentions = mutableListOf>() - - var startFrom = 0 - - while (true) { - val leftBracketIndex = newMessageText.indexOf('[', startFrom) - val verticalLineIndex = newMessageText.indexOf('|', startFrom) - val rightBracketIndex = newMessageText.indexOf(']', startFrom) - - if (leftBracketIndex == -1 || - verticalLineIndex == -1 || - rightBracketIndex == -1 - ) break - - val idPart = newMessageText.substring(leftBracketIndex + 1, verticalLineIndex) - - val actualId = idPart.substring(2, idPart.length).toIntOrNull() ?: -1 - - if (!idPart.matches(Regex("^id(\\d+)\$")) || rightBracketIndex - verticalLineIndex < 2) { - break - } - - val text = newMessageText.substring(verticalLineIndex + 1, rightBracketIndex) - - val str = "[$idPart|$text]" - - mentions += str to text - - idsIndexes += Triple(actualId, leftBracketIndex, leftBracketIndex + text.length) - - startFrom = rightBracketIndex + 1 - } - - idsIndexes.reverse() - - mentions.forEachIndexed { index, pair -> - val old = pair.first - val new = pair.second - - val oldIndexStart = newMessageText.indexOf(old) - - idsIndexes[index].copy( - second = oldIndexStart, - third = oldIndexStart + new.length - ).let { idsIndexes[index] = it } - - newMessageText = newMessageText.replace(old, new) - } - - val spanBuilder = SpannableStringBuilder(newMessageText) - - idsIndexes.forEach { triple -> - val id = triple.first - val start = triple.second - val end = triple.third - - spanBuilder.setSpan( - createClickableSpan(id, mentionColor, onMentionClick), - start, - end, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } - - return spanBuilder - } - - private fun createClickableSpan( - id: Int, - mentionColor: Int, - onMentionClick: ((id: Int) -> Unit)? = null, - ): ClickableSpan { - return object : ClickableSpan() { - override fun onClick(widget: View) { - widget.cancelPendingInputEvents() - - onMentionClick?.invoke(id) - } - - override fun updateDrawState(ds: TextPaint) { - ds.color = mentionColor -// ds.typeface = Typeface.defaultFromStyle(Typeface.BOLD) - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/base/ApiError.kt b/app/src/main/kotlin/com/meloda/fast/api/base/ApiError.kt deleted file mode 100644 index ae38560f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/base/ApiError.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.meloda.fast.api.base - -import com.google.gson.Gson -import com.google.gson.annotations.SerializedName -import okio.IOException - -open class ApiError( - @SerializedName("error", alternate = ["error_code"]) - val error: String? = null, - @SerializedName("error_msg", alternate = ["error_description"]) - open val errorMessage: String? = null, - @SerializedName("error_type") - val errorType: String? = null, - val throwable: Throwable? = null -) : IOException() { - - override fun toString(): String { - return Gson().toJson(this) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/base/ApiResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/base/ApiResponse.kt deleted file mode 100644 index 51c3cd35..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/base/ApiResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.meloda.fast.api.base - -data class ApiResponse( - val error: ApiError? = null, - val response: T? = null -) { - val isSuccessful get() = error == null && response != null -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/base/AttachmentClassNameIsEmptyException.kt b/app/src/main/kotlin/com/meloda/fast/api/base/AttachmentClassNameIsEmptyException.kt deleted file mode 100644 index 2aad4520..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/base/AttachmentClassNameIsEmptyException.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.api.base - -import com.meloda.fast.api.model.attachments.VkAttachment -import okio.IOException - -class AttachmentClassNameIsEmptyException(attachment: VkAttachment) : - IOException( - "attachment ${attachment.javaClass.name} does not have declared field \"className\"" - ) diff --git a/app/src/main/kotlin/com/meloda/fast/api/longpoll/LongPollEvent.kt b/app/src/main/kotlin/com/meloda/fast/api/longpoll/LongPollEvent.kt deleted file mode 100644 index 3637bd5f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/longpoll/LongPollEvent.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.meloda.fast.api.longpoll - -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.model.VkUser - -sealed class LongPollEvent { - - data class VkMessageNewEvent( - val message: VkMessage, - val profiles: HashMap, - val groups: HashMap, - ) : LongPollEvent() - - data class VkMessageEditEvent(val message: VkMessage) : LongPollEvent() - - data class VkMessageReadIncomingEvent( - val peerId: Int, - val messageId: Int, - val unreadCount: Int, - ) : LongPollEvent() - - data class VkMessageReadOutgoingEvent( - val peerId: Int, - val messageId: Int, - val unreadCount: Int, - ) : LongPollEvent() - - data class VkConversationPinStateChangedEvent( - val peerId: Int, - val majorId: Int, - ) : LongPollEvent() - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/longpoll/LongPollUpdatesParser.kt b/app/src/main/kotlin/com/meloda/fast/api/longpoll/LongPollUpdatesParser.kt deleted file mode 100644 index 324d446f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/longpoll/LongPollUpdatesParser.kt +++ /dev/null @@ -1,300 +0,0 @@ -package com.meloda.fast.api.longpoll - -import android.util.Log -import com.google.gson.JsonArray -import com.meloda.fast.api.ApiEvent -import com.meloda.fast.api.VKConstants -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.messages.MessagesGetByIdRequest -import com.meloda.fast.base.viewmodel.VkEventCallback -import com.meloda.fast.data.messages.MessagesRepository -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -@Suppress("UNCHECKED_CAST", "MemberVisibilityCanBePrivate") -class LongPollUpdatesParser(private val messagesRepository: MessagesRepository) : CoroutineScope { - - private val job = SupervisorJob() - - private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> - Log.d("LongPollUpdatesParser", "error: $throwable") - throwable.printStackTrace() - } - - override val coroutineContext: CoroutineContext - get() = Dispatchers.Default + job + exceptionHandler - - private val listenersMap: MutableMap>> = - mutableMapOf() - - fun parseNextUpdate(event: JsonArray) { - val eventId = event[0].asInt - val eventType: ApiEvent? = ApiEvent.parse(eventId) - - if (eventType == null) { - Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event") - return - } - - when (eventType) { - ApiEvent.MessageSetFlags -> parseMessageSetFlags(eventType, event) - ApiEvent.MessageClearFlags -> parseMessageClearFlags(eventType, event) - ApiEvent.MessageNew -> parseMessageNew(eventType, event) - ApiEvent.MessageEdit -> parseMessageEdit(eventType, event) - ApiEvent.MessageReadIncoming -> parseMessageReadIncoming(eventType, event) - ApiEvent.MessageReadOutgoing -> parseMessageReadOutgoing(eventType, event) - ApiEvent.MessagesDeleted -> parseMessagesDeleted(eventType, event) - ApiEvent.PinUnpinConversation -> parseConversationPinStateChanged(eventType, event) - ApiEvent.PrivateTyping -> onNewEvent(eventType, event) - ApiEvent.ChatTyping -> onNewEvent(eventType, event) - ApiEvent.OneMoreTyping -> onNewEvent(eventType, event) - ApiEvent.VoiceRecording -> onNewEvent(eventType, event) - ApiEvent.PhotoUploading -> onNewEvent(eventType, event) - ApiEvent.VideoUploading -> onNewEvent(eventType, event) - ApiEvent.FileUploading -> onNewEvent(eventType, event) - ApiEvent.UnreadCountUpdate -> onNewEvent(eventType, event) - } - - } - - private fun onNewEvent(eventType: ApiEvent, event: JsonArray) { - Log.d("LongPollUpdatesParser", "newEvent: $eventType: $event") - } - - private fun parseConversationPinStateChanged(eventType: ApiEvent, event: JsonArray) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - - val peerId = event[1].asInt - val majorId = event[2].asInt - - launch { - listenersMap[ApiEvent.PinUnpinConversation]?.let { listeners -> - listeners.forEach { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent( - LongPollEvent.VkConversationPinStateChangedEvent( - peerId = peerId, - majorId = majorId - ) - ) - } - } - } - } - - private fun parseMessageSetFlags(eventType: ApiEvent, event: JsonArray) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - } - - private fun parseMessageClearFlags(eventType: ApiEvent, event: JsonArray) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - } - - private fun parseMessageNew(eventType: ApiEvent, event: JsonArray) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - val messageId = event[1].asInt - - launch { - val newMessageEvent: LongPollEvent.VkMessageNewEvent = - loadNormalMessage( - eventType, - messageId - ) - - listenersMap[ApiEvent.MessageNew]?.let { - it.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent(newMessageEvent) - } - } - } - } - - private fun parseMessageEdit(eventType: ApiEvent, event: JsonArray) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - val messageId = event[1].asInt - - launch { - val editedMessageEvent: LongPollEvent.VkMessageEditEvent = - loadNormalMessage( - eventType, - messageId - ) - - listenersMap[ApiEvent.MessageEdit]?.let { - it.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent(editedMessageEvent) - } - } - } - } - - private fun parseMessageReadIncoming(eventType: ApiEvent, event: JsonArray) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - val peerId = event[1].asInt - val messageId = event[2].asInt - val unreadCount = event[3].asInt - - launch { - listenersMap[ApiEvent.MessageReadIncoming]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent( - LongPollEvent.VkMessageReadIncomingEvent( - peerId = peerId, - messageId = messageId, - unreadCount = unreadCount - ) - ) - } - } - } - } - - private fun parseMessageReadOutgoing(eventType: ApiEvent, event: JsonArray) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - val peerId = event[1].asInt - val messageId = event[2].asInt - val unreadCount = event[3].asInt - - launch { - listenersMap[ApiEvent.MessageReadOutgoing]?.let { listeners -> - listeners.map { vkEventCallback -> - (vkEventCallback as VkEventCallback) - .onEvent( - LongPollEvent.VkMessageReadOutgoingEvent( - peerId = peerId, - messageId = messageId, - unreadCount = unreadCount - ) - ) - } - } - } - } - - private fun parseMessagesDeleted(eventType: ApiEvent, event: JsonArray) { - Log.d("LongPollUpdatesParser", "$eventType: $event") - } - - private suspend fun loadNormalMessage(eventType: ApiEvent, messageId: Int) = - coroutineScope { - suspendCoroutine { - launch { - val normalMessageResponse = messagesRepository.getById( - MessagesGetByIdRequest( - messagesIds = listOf(messageId), - extended = true, - fields = VKConstants.ALL_FIELDS - ) - ) - - if (normalMessageResponse.isError()) { - normalMessageResponse.error.throwable?.run { throw this } - } - - val messagesResponse = - (normalMessageResponse as? ApiAnswer.Success)?.data?.response - ?: return@launch - - val messagesList = messagesResponse.items - if (messagesList.isEmpty()) return@launch - - val normalMessage = messagesList[0].asVkMessage() - messagesRepository.store(listOf(normalMessage)) - - val profiles = hashMapOf() - messagesResponse.profiles?.forEach { baseUser -> - baseUser.mapToDomain().let { user -> profiles[user.id] = user } - } - - val groups = hashMapOf() - messagesResponse.groups?.forEach { baseGroup -> - baseGroup.mapToDomain().let { group -> groups[group.id] = group } - } - - val resumeValue: LongPollEvent? = when (eventType) { - ApiEvent.MessageNew -> - LongPollEvent.VkMessageNewEvent( - normalMessage, - profiles, - groups - ) - ApiEvent.MessageEdit -> LongPollEvent.VkMessageEditEvent(normalMessage) - else -> null - } - - resumeValue?.let { value -> it.resume(value as T) } - } - } - } - - - private fun registerListener(eventType: ApiEvent, listener: VkEventCallback) { - listenersMap.let { map -> - map[eventType] = (map[eventType] ?: mutableListOf()).also { - it.add(listener) - } - } - } - - fun onConversationPinStateChanged(listener: VkEventCallback) { - registerListener(ApiEvent.PinUnpinConversation, listener) - } - - fun onConversationPinStateChanged(block: (LongPollEvent.VkConversationPinStateChangedEvent) -> Unit) { - onConversationPinStateChanged(assembleEventCallback(block)) - } - - fun onMessageIncomingRead(listener: VkEventCallback) { - registerListener(ApiEvent.MessageReadIncoming, listener) - } - - fun onMessageIncomingRead(block: (LongPollEvent.VkMessageReadIncomingEvent) -> Unit) { - onMessageIncomingRead(assembleEventCallback(block)) - } - - fun onMessageOutgoingRead(listener: VkEventCallback) { - registerListener(ApiEvent.MessageReadOutgoing, listener) - } - - fun onMessageOutgoingRead(block: (LongPollEvent.VkMessageReadOutgoingEvent) -> Unit) { - onMessageOutgoingRead(assembleEventCallback(block)) - } - - fun onNewMessage(listener: VkEventCallback) { - registerListener(ApiEvent.MessageNew, listener) - } - - fun onNewMessage(block: (LongPollEvent.VkMessageNewEvent) -> Unit) { - onNewMessage(assembleEventCallback(block)) - } - - fun onMessageEdited(listener: VkEventCallback) { - registerListener(ApiEvent.MessageEdit, listener) - } - - fun onMessageEdited(block: (LongPollEvent.VkMessageEditEvent) -> Unit) { - onMessageEdited(assembleEventCallback(block)) - } - - fun clearListeners() { - listenersMap.clear() - } -} - -internal inline fun assembleEventCallback( - crossinline block: (R) -> Unit, -): VkEventCallback { - return VkEventCallback { event -> block.invoke(event) } -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/ActionState.kt b/app/src/main/kotlin/com/meloda/fast/api/model/ActionState.kt deleted file mode 100644 index ece31811..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/ActionState.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.meloda.fast.api.model - -sealed class ActionState { - object Phantom : ActionState() - object CallInProgress : ActionState() - object None : ActionState() - - companion object { - fun parse(isPhantom: Boolean, isCallInProgress: Boolean): ActionState { - return when { - isPhantom -> Phantom - isCallInProgress -> CallInProgress - else -> None - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/ConversationPeerType.kt b/app/src/main/kotlin/com/meloda/fast/api/model/ConversationPeerType.kt deleted file mode 100644 index 25c3bb53..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/ConversationPeerType.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.meloda.fast.api.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -sealed class ConversationPeerType : Parcelable { - object User : ConversationPeerType() - object Group : ConversationPeerType() - object Chat : ConversationPeerType() - - fun isUser() = this == User - fun isGroup() = this == Group - fun isChat() = this == Chat - - companion object { - fun parse(type: String): ConversationPeerType { - return when (type) { - "user" -> User - "group" -> Group - else -> Chat - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/VkChatMember.kt b/app/src/main/kotlin/com/meloda/fast/api/model/VkChatMember.kt deleted file mode 100644 index 1a513309..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/VkChatMember.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.meloda.fast.api.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VkChatMember( - val memberId: Int, - val invitedBy: Int, - val joinDate: Int, - val isAdmin: Boolean, - val isOwner: Boolean, - val canKick: Boolean -) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/VkGroup.kt b/app/src/main/kotlin/com/meloda/fast/api/model/VkGroup.kt deleted file mode 100644 index 0e3469a4..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/VkGroup.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.meloda.fast.api.model - -import android.os.Parcelable -import androidx.room.Entity -import androidx.room.PrimaryKey -import kotlinx.parcelize.Parcelize - -@Entity(tableName = "groups") -@Parcelize -data class VkGroup( - @PrimaryKey(autoGenerate = false) - val id: Int, - val name: String, - val screenName: String, - val photo200: String?, - val membersCount: Int? -) : Parcelable { - - override fun toString() = name.trim() - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/VkMessage.kt b/app/src/main/kotlin/com/meloda/fast/api/model/VkMessage.kt deleted file mode 100644 index 55373d72..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/VkMessage.kt +++ /dev/null @@ -1,132 +0,0 @@ -package com.meloda.fast.api.model - -import androidx.room.Entity -import androidx.room.Ignore -import androidx.room.PrimaryKey -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.VKConstants -import com.meloda.fast.api.model.attachments.VkAttachment -import com.meloda.fast.api.model.base.BaseVkMessage -import com.meloda.fast.api.model.domain.VkConversationDomain -import com.meloda.fast.model.SelectableItem -import com.meloda.fast.util.TimeUtils -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -// TODO: 05.08.2023, Danil Nikolaev: create other class for storing in database -@Entity(tableName = "messages") -@Parcelize -data class VkMessage constructor( - @PrimaryKey(autoGenerate = false) - var id: Int, - var text: String? = null, - val isOut: Boolean, - val peerId: Int, - val fromId: Int, - val date: Int, - val randomId: Int, - val action: String? = null, - val actionMemberId: Int? = null, - val actionText: String? = null, - val actionConversationMessageId: Int? = null, - val actionMessage: String? = null, - - var updateTime: Int? = null, - - var important: Boolean = false, - - var forwards: List? = null, - var attachments: List? = null, - var replyMessage: VkMessage? = null, - - val geo: BaseVkMessage.Geo? = null, -) : SelectableItem() { - - @Ignore - @IgnoredOnParcel - var user: VkUser? = null - - @Ignore - @IgnoredOnParcel - var group: VkGroup? = null - - @Ignore - @IgnoredOnParcel - var actionUser: VkUser? = null - - @Ignore - @IgnoredOnParcel - var actionGroup: VkGroup? = null - - @Ignore - @IgnoredOnParcel - var state: State = State.Sent - - fun isPeerChat() = peerId > 2_000_000_000 - - fun isUser() = fromId > 0 - - fun isGroup() = fromId < 0 - - fun isRead(conversation: VkConversationDomain) = - if (isOut) { - conversation.outRead - id >= 0 - } else { - conversation.inRead - id >= 0 - } - - fun getPreparedAction(): Action? { - if (action == null) return null - return Action.parse(action) - } - - fun canEdit() = - fromId == UserConfig.userId && - (attachments == null || - !VKConstants.restrictedToEditAttachments.contains( - requireNotNull(attachments).first().javaClass - )) && - (System.currentTimeMillis() / 1000 - date.toLong() < TimeUtils.OneDayInSeconds) - - fun hasAttachments(): Boolean = !attachments.isNullOrEmpty() - - fun hasReply(): Boolean = replyMessage != null - - fun hasForwards(): Boolean = !forwards.isNullOrEmpty() - - fun hasGeo(): Boolean = geo != null - - fun isUpdated(): Boolean = updateTime != null && requireNotNull(updateTime) > 0 - - fun isSending(): Boolean = state == State.Sending - - fun isError(): Boolean = state == State.Error - - fun isSent(): Boolean = state == State.Sent - - enum class Action(val value: String) { - CHAT_CREATE("chat_create"), - CHAT_PHOTO_UPDATE("chat_photo_update"), - CHAT_PHOTO_REMOVE("chat_photo_remove"), - CHAT_TITLE_UPDATE("chat_title_update"), - CHAT_PIN_MESSAGE("chat_pin_message"), - CHAT_UNPIN_MESSAGE("chat_unpin_message"), - CHAT_INVITE_USER("chat_invite_user"), - CHAT_INVITE_USER_BY_LINK("chat_invite_user_by_link"), - CHAT_KICK_USER("chat_kick_user"), - CHAT_SCREENSHOT("chat_screenshot"), - - CHAT_INVITE_USER_BY_CALL("chat_invite_user_by_call"), - CHAT_INVITE_USER_BY_CALL_LINK("chat_invite_user_by_call_join_link"), - CHAT_STYLE_UPDATE("conversation_style_update"); - - companion object { - fun parse(value: String?): Action? = values().firstOrNull { it.value == value } - } - } - - enum class State { - Sending, Sent, Error - } - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/VkUser.kt b/app/src/main/kotlin/com/meloda/fast/api/model/VkUser.kt deleted file mode 100644 index fdd2f9e2..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/VkUser.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.api.model - -import android.os.Parcelable -import androidx.room.Entity -import androidx.room.PrimaryKey -import kotlinx.parcelize.Parcelize - -@Entity(tableName = "users") -@Parcelize -data class VkUser( - @PrimaryKey(autoGenerate = false) - val id: Int, - val firstName: String, - val lastName: String, - val online: Boolean, - val photo200: String?, - val lastSeen: Int?, - val lastSeenStatus: String?, - val birthday: String? -) : Parcelable { - - override fun toString() = fullName - - val fullName get() = "$firstName $lastName".trim() - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAttachment.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAttachment.kt deleted file mode 100644 index 71702bfb..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAttachment.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.meloda.fast.api.model.attachments - -import android.os.Parcelable -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -open class VkAttachment : Parcelable { - - open fun asString(withAccessKey: Boolean = true) = "" - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAudio.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAudio.kt deleted file mode 100644 index 1427cf2f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAudio.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.meloda.fast.api.model.attachments - -import com.meloda.fast.api.VkUtils -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VkAudio( - val id: Int, - val ownerId: Int, - val title: String, - val artist: String, - val url: String, - val duration: Int, - val accessKey: String? -) : VkAttachment() { - - @IgnoredOnParcel - val className: String = this::class.java.name - - override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString( - attachmentClass = this::class.java, - id = id, - ownerId = ownerId, - withAccessKey = withAccessKey, - accessKey = accessKey - ) -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCall.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCall.kt deleted file mode 100644 index 7a6c4002..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCall.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.meloda.fast.api.model.attachments - -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VkCall( - val initiatorId: Int, - val receiverId: Int, - val state: String, - val time: Int, - val duration: Int, - val isVideo: Boolean -) : VkAttachment() { - - @IgnoredOnParcel - val className: String = this::class.java.name - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCurator.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCurator.kt deleted file mode 100644 index dd656e4d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCurator.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.meloda.fast.api.model.attachments - -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VkCurator( - val id: Int, -) : VkAttachment() { - - @IgnoredOnParcel - val className: String = this::class.java.name -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkEvent.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkEvent.kt deleted file mode 100644 index debda085..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkEvent.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.meloda.fast.api.model.attachments - -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VkEvent( - val id: Int -) : VkAttachment() diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkFile.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkFile.kt deleted file mode 100644 index 2766e61b..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkFile.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.meloda.fast.api.model.attachments - -import com.meloda.fast.api.VkUtils -import com.meloda.fast.api.model.base.attachments.BaseVkFile -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VkFile( - val id: Int, - val ownerId: Int, - val title: String, - val ext: String, - val size: Int, - val url: String, - val accessKey: String?, - val preview: BaseVkFile.Preview? -) : VkAttachment() { - - @IgnoredOnParcel - val className: String = this::class.java.name - - override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString( - attachmentClass = this::class.java, - id = id, - ownerId = ownerId, - withAccessKey = withAccessKey, - accessKey = accessKey - ) -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGift.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGift.kt deleted file mode 100644 index 6be29fca..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGift.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.meloda.fast.api.model.attachments - -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VkGift( - val id: Int, - val thumb256: String?, - val thumb96: String?, - val thumb48: String -) : VkAttachment() { - - @IgnoredOnParcel - val className: String = this::class.java.name -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGraffiti.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGraffiti.kt deleted file mode 100644 index 2f4ddae9..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGraffiti.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.meloda.fast.api.model.attachments - -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VkGraffiti( - val id: Int, - val ownerId: Int, - val url: String, - val width: Int, - val height: Int, - val accessKey: String -) : VkAttachment() { - - @IgnoredOnParcel - val className: String = this::class.java.name -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGroupCall.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGroupCall.kt deleted file mode 100644 index 2a0aa581..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGroupCall.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.meloda.fast.api.model.attachments - -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VkGroupCall( - val initiatorId: Int -) : VkAttachment() { - - @IgnoredOnParcel - val className: String = this::class.java.name -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkLink.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkLink.kt deleted file mode 100644 index 321c71ac..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkLink.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.meloda.fast.api.model.attachments - -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VkLink( - val url: String, - val title: String?, - val caption: String?, - val photo: VkPhoto?, - val target: String?, - val isFavorite: Boolean -) : VkAttachment() { - - @IgnoredOnParcel - val className: String = this::class.java.name -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkMiniApp.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkMiniApp.kt deleted file mode 100644 index a65fb1f1..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkMiniApp.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.meloda.fast.api.model.attachments - -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VkMiniApp( - val link: String -) : VkAttachment() { - - @IgnoredOnParcel - val className: String = this::class.java.name -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPoll.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPoll.kt deleted file mode 100644 index 4bee6a71..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPoll.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.meloda.fast.api.model.attachments - -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VkPoll( - val id: Int -) : VkAttachment() { - - @IgnoredOnParcel - val className: String = this::class.java.name -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkSticker.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkSticker.kt deleted file mode 100644 index da6c4c82..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkSticker.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.api.model.attachments - -import com.meloda.fast.api.model.base.attachments.BaseVkSticker -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VkSticker( - val id: Int, - val productId: Int, - val images: List, - val backgroundImages: List -) : VkAttachment() { - - @IgnoredOnParcel - val className: String = this::class.java.name - - fun urlForSize(size: Int): String? { - for (image in images) { - if (image.width == size) return image.url - } - - return null - } - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkStory.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkStory.kt deleted file mode 100644 index c6862706..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkStory.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.meloda.fast.api.model.attachments - -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VkStory( - val id: Int, - val ownerId: Int, - val date: Int, - val photo: VkPhoto? -) : VkAttachment() { - - fun isFromUser() = ownerId > 0 - - fun isFromGroup() = ownerId < 0 - - @IgnoredOnParcel - val className: String = this::class.java.name - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVoiceMessage.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVoiceMessage.kt deleted file mode 100644 index 557f4c77..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVoiceMessage.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.meloda.fast.api.model.attachments - -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VkVoiceMessage( - val id: Int, - val ownerId: Int, - val duration: Int, - val waveform: List, - val linkOgg: String, - val linkMp3: String, - val accessKey: String, - val transcriptState: String?, - val transcript: String? -) : VkAttachment() { - - @IgnoredOnParcel - val className: String = this::class.java.name - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWall.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWall.kt deleted file mode 100644 index bd24073a..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWall.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.meloda.fast.api.model.attachments - -import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VkWall( - val id: Int, - val fromId: Int, - val toId: Int, - val date: Int, - val text: String, - val attachments: List?, - val comments: Int?, - val likes: Int?, - val reposts: Int?, - val views: Int?, - val isFavorite: Boolean, - val accessKey: String? -) : VkAttachment() { - - @IgnoredOnParcel - val className: String = this::class.java.name -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWallReply.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWallReply.kt deleted file mode 100644 index 110145eb..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWallReply.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.meloda.fast.api.model.attachments - -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VkWallReply( - val id: Int -) : VkAttachment() { - - @IgnoredOnParcel - val className: String = this::class.java.name -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWidget.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWidget.kt deleted file mode 100644 index 7edb329f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWidget.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.meloda.fast.api.model.attachments - -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VkWidget( - val id: Int -) : VkAttachment() { - - @IgnoredOnParcel - val className: String = this::class.java.name -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkChat.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkChat.kt deleted file mode 100644 index 1f10f4c5..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkChat.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.meloda.fast.api.model.base - -import android.os.Parcelable -import com.meloda.fast.api.model.VkChat -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkChat( - val type: String, - val title: String, - val admin_id: Int, - val members_count: Int, - val id: Int, - val photo_50: String, - val photo_100: String, - val photo_200: String, - val is_default_photo: Boolean, - val push_settings: PushSettings -) : Parcelable { - - fun asVkChat() = VkChat( - type = type, - title = title, - adminId = admin_id, - membersCount = members_count, - id = id, - photo50 = photo_50, - photo100 = photo_100, - photo200 = photo_200, - isDefaultPhoto = is_default_photo - ) - - @Parcelize - data class PushSettings( - val sound: Int, - val disabled_until: Int - ) : Parcelable -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkChatMember.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkChatMember.kt deleted file mode 100644 index 139664d9..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkChatMember.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.api.model.base - -import android.os.Parcelable -import com.meloda.fast.api.model.VkChatMember -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkChatMember( - val member_id: Int, - val invited_by: Int, - val join_date: Int, - val is_admin: Boolean?, - val is_owner: Boolean?, - val can_kick: Boolean? -) : Parcelable { - - fun asVkChatMember() = VkChatMember( - memberId = member_id, - invitedBy = invited_by, - joinDate = join_date, - isAdmin = is_admin == true, - isOwner = is_owner == true, - canKick = can_kick == true - ) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkGroup.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkGroup.kt deleted file mode 100644 index e76b3ea7..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkGroup.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.meloda.fast.api.model.base - -import android.os.Parcelable -import com.meloda.fast.api.model.VkGroup -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkGroup( - val id: Int, - val name: String, - val screen_name: String, - val is_closed: Int, - val type: String, - val is_admin: Int, - val is_member: Int, - val is_advertiser: Int, - val photo_50: String?, - val photo_100: String?, - val photo_200: String?, - val members_count: Int? -) : Parcelable { - - fun mapToDomain() = VkGroup( - id = -id, - name = name, - screenName = screen_name, - photo200 = photo_200, - membersCount = members_count - ) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkLongPoll.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkLongPoll.kt deleted file mode 100644 index cf5d0814..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkLongPoll.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.meloda.fast.api.model.base - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkLongPoll( - val server: String, - val key: String, - val ts: Int, - val pts: Int -) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkMessage.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkMessage.kt deleted file mode 100644 index 46a5b7fa..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkMessage.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.meloda.fast.api.model.base - -import android.os.Parcelable -import com.meloda.fast.api.VkUtils -import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkMessage( - val id: Int, - val peer_id: Int, - val date: Int, - val from_id: Int, - val out: Int, - val text: String, - val conversation_message_id: Int, - val fwd_messages: List? = emptyList(), - val important: Boolean, - val random_id: Int, - val attachments: List = emptyList(), - val is_hidden: Boolean, - val payload: String, - val geo: Geo?, - val action: Action?, - val ttl: Int, - val reply_message: BaseVkMessage?, - val update_time: Int? -) : Parcelable { - - fun asVkMessage() = VkMessage( - id = id, - text = text.ifBlank { null }, - isOut = out == 1, - peerId = peer_id, - fromId = from_id, - date = date, - randomId = random_id, - action = action?.type, - actionMemberId = action?.member_id, - actionText = action?.text, - actionConversationMessageId = action?.conversation_message_id, - actionMessage = action?.message, - geo = geo, - important = important, - updateTime = update_time - ).also { - it.attachments = VkUtils.parseAttachments(attachments) - it.forwards = VkUtils.parseForwards(fwd_messages) - it.replyMessage = VkUtils.parseReplyMessage(reply_message) - } - - @Parcelize - data class Geo( - val type: String, - val coordinates: Coordinates, - val place: Place - ) : Parcelable { - - @Parcelize - data class Coordinates(val latitude: Float, val longitude: Float) : Parcelable - - @Parcelize - data class Place(val country: String, val city: String, val title: String) : Parcelable - } - - @Parcelize - data class Action( - val type: String, - val member_id: Int?, - val text: String?, - val conversation_message_id: Int?, - val message: String? - ) : Parcelable - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkUser.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkUser.kt deleted file mode 100644 index 322ce4d2..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkUser.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.meloda.fast.api.model.base - -import android.os.Parcelable -import com.meloda.fast.api.model.VkUser -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkUser( - val id: Int, - val first_name: String, - val last_name: String, - val can_access_closed: Boolean, - val is_closed: Boolean, - val can_invite_to_chats: Boolean, - val sex: Int?, - val photo_50: String?, - val photo_100: String?, - val photo_200: String?, - val online: Int?, - val online_info: OnlineInfo?, - val screen_name: String, - val bdate: String? - //...other fields -) : Parcelable { - - @Parcelize - data class OnlineInfo( - val visible: Boolean, - val status: String, - val last_seen: Int?, - val is_online: Boolean?, - val online_mobile: Boolean?, - val app_id: Int? - ) : Parcelable - - fun mapToDomain() = VkUser( - id = id, - firstName = first_name, - lastName = last_name, - online = online == 1, - photo200 = photo_200, - lastSeen = online_info?.last_seen, - lastSeenStatus = online_info?.status, - birthday = bdate - ) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAttachmentItem.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAttachmentItem.kt deleted file mode 100644 index 51f0138f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAttachmentItem.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.meloda.fast.api.model.base.attachments - -import android.os.Parcelable -import android.util.Log -import com.google.gson.annotations.SerializedName -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkAttachmentItem( - val type: String, - val photo: BaseVkPhoto?, - val video: BaseVkVideo?, - val audio: BaseVkAudio?, - @SerializedName("doc") - val file: BaseVkFile?, - val link: BaseVkLink?, - @SerializedName("mini_app") - val miniApp: BaseVkMiniApp?, - @SerializedName("audio_message") - val voiceMessage: BaseVkVoiceMessage?, - val sticker: BaseVkSticker?, - val gift: BaseVkGift?, - val wall: BaseVkWall?, - val graffiti: BaseVkGraffiti?, - val poll: BaseVkPoll?, - @SerializedName("wall_reply") - val wallReply: BaseVkWallReply?, - val call: BaseVkCall?, - @SerializedName("group_call_in_progress") - val groupCall: BaseVkGroupCall?, - val curator: BaseVkCurator?, - val event: BaseVkEvent?, - val story: BaseVkStory?, - val widget: BaseVkWidget? -) : Parcelable { - - fun getPreparedType() = AttachmentType.parse(type) - - enum class AttachmentType(var value: String) { - Unknown("unknown"), - Photo("photo"), - Video("video"), - Audio("audio"), - File("doc"), - Link("link"), - Voice("audio_message"), - MiniApp("mini_app"), - Sticker("sticker"), - Gift("gift"), - Wall("wall"), - Graffiti("graffiti"), - Poll("poll"), - WallReply("wall_reply"), - Call("call"), - GroupCallInProgress("group_call_in_progress"), - Curator("curator"), - Event("event"), - Story("story"), - Widget("widget") - ; - - companion object { - fun parse(value: String): AttachmentType { - val parsedValue = values().firstOrNull { it.value == value } ?: Unknown - - if (parsedValue == Unknown) { - Log.e("AttachmentType", "Unknown attachment type: $value") - } - - return parsedValue - } - } - } - -} - -abstract class BaseVkAttachment : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAudio.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAudio.kt deleted file mode 100644 index 09de47b9..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAudio.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.meloda.fast.api.model.base.attachments - -import android.os.Parcelable -import com.meloda.fast.api.model.attachments.VkAudio -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkAudio( - val id: Int, - val title: String, - val artist: String, - val duration: Int, - val url: String, - val date: Int, - val owner_id: Int, - val access_key: String?, - val is_explicit: Boolean, - val is_focus_track: Boolean, - val is_licensed: Boolean, - val track_code: String, - val genre_id: Int, - val album: Album, - val short_videos_allowed: Boolean, - val stories_allowed: Boolean, - val stories_cover_allowed: Boolean -) : BaseVkAttachment() { - - fun asVkAudio() = VkAudio( - id = id, - ownerId = owner_id, - title = title, - artist = artist, - url = url, - duration = duration, - accessKey = access_key - ) - - @Parcelize - data class Album( - val id: Int, - val title: String, - val owner_id: Int, - val access_key: String, - val thumb: Thumb - ) : Parcelable { - - @Parcelize - data class Thumb( - val width: Int, - val height: Int, - val photo_34: String, - val photo_68: String, - val photo_135: String, - val photo_270: String, - val photo_300: String, - val photo_600: String, - val photo_1200: String - ) : Parcelable - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCall.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCall.kt deleted file mode 100644 index 2bbde082..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCall.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.api.model.base.attachments - -import android.os.Parcelable -import com.meloda.fast.api.model.attachments.VkCall -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkCall( - val initiator_id: Int, - val receiver_id: Int, - val state: String, - val time: Int, - val duration: Int, - val video: Boolean -) : Parcelable { - - fun asVkCall() = VkCall( - initiatorId = initiator_id, - receiverId = receiver_id, - state = state, - time = time, - duration = duration, - isVideo = video - ) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCurator.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCurator.kt deleted file mode 100644 index a84b6e85..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCurator.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.meloda.fast.api.model.base.attachments - -import android.os.Parcelable -import com.meloda.fast.api.model.attachments.VkCurator -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkCurator( - val id: Int, - val name: String, - val description: String, - val url: String, - val photo: List -) : BaseVkAttachment() { - - fun asVkCurator() = VkCurator( - id = id - ) - - @Parcelize - data class Photo( - val height: Int, - val url: String, - val width: String - ) : Parcelable - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkEvent.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkEvent.kt deleted file mode 100644 index a1b09ce8..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkEvent.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.meloda.fast.api.model.base.attachments - -import com.meloda.fast.api.model.attachments.VkEvent -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkEvent( - val button_text: String, - val id: Int, - val is_favorite: Boolean, - val text: String, - val address: String, - val friends: List = emptyList(), - val member_status: Int, - val time: Int -) : BaseVkAttachment() { - - fun asVkEvent() = VkEvent(id = id) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkFile.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkFile.kt deleted file mode 100644 index edc8b96d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkFile.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.meloda.fast.api.model.base.attachments - -import android.os.Parcelable -import com.meloda.fast.api.model.attachments.VkFile -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkFile( - val id: Int, - val owner_id: Int, - val title: String, - val size: Int, - val ext: String, - val date: Int, - val type: Int, - val url: String, - val preview: Preview?, - val ic_licensed: Int, - val access_key: String?, - val web_preview_url: String? -) : BaseVkAttachment() { - - fun asVkFile() = VkFile( - id = id, - ownerId = owner_id, - title = title, - ext = ext, - url = url, - size = size, - accessKey = access_key, - preview = preview - ) - - @Parcelize - data class Preview( - val photo: Photo?, - val video: Video? - ) : Parcelable { - - @Parcelize - data class Photo(val sizes: List) : Parcelable { - - @Parcelize - data class Size( - val height: Int, - val width: Int, - val type: String, - val src: String - ) : Parcelable - - } - - @Parcelize - data class Video( - val src: String, - val width: Int, - val height: Int, - val file_size: Int - ) : Parcelable - - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGift.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGift.kt deleted file mode 100644 index 29e646b9..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGift.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.meloda.fast.api.model.base.attachments - -import android.os.Parcelable -import com.meloda.fast.api.model.attachments.VkGift -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkGift( - val id: Int, - val thumb_256: String?, - val thumb_96: String?, - val thumb_48: String -) : Parcelable { - - fun asVkGift() = VkGift( - id = id, - thumb256 = thumb_256, - thumb96 = thumb_96, - thumb48 = thumb_48 - ) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGraffiti.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGraffiti.kt deleted file mode 100644 index c5e841ef..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGraffiti.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.api.model.base.attachments - -import android.os.Parcelable -import com.meloda.fast.api.model.attachments.VkGraffiti -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkGraffiti( - val id: Int, - val owner_id: Int, - val url: String, - val width: Int, - val height: Int, - val access_key: String -) : Parcelable { - - fun asVkGraffiti() = VkGraffiti( - id = id, - ownerId = owner_id, - url = url, - width = width, - height = height, - accessKey = access_key - ) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGroupCall.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGroupCall.kt deleted file mode 100644 index e9ff17c0..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGroupCall.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.meloda.fast.api.model.base.attachments - -import android.os.Parcelable -import com.meloda.fast.api.model.attachments.VkGroupCall -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkGroupCall( - val initiator_id: Int, - val join_link: String, - val participants: Participants -) : Parcelable { - - @Parcelize - data class Participants( - val list: List, - val count: Int - ) : Parcelable - - fun asVkGroupCall() = VkGroupCall(initiatorId = initiator_id) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkLink.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkLink.kt deleted file mode 100644 index 4f1b59c5..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkLink.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.meloda.fast.api.model.base.attachments - -import com.meloda.fast.api.model.attachments.VkLink -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkLink( - val url: String, - val title: String?, - val caption: String?, - val photo: BaseVkPhoto?, - val target: String?, - val is_favorite: Boolean -) : BaseVkAttachment() { - - fun asVkLink() = VkLink( - url = url, - title = title, - caption = caption, - photo = photo?.asVkPhoto(), - target = target, - isFavorite = is_favorite - ) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkMiniApp.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkMiniApp.kt deleted file mode 100644 index 8e857850..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkMiniApp.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.meloda.fast.api.model.base.attachments - -import android.os.Parcelable -import com.google.gson.annotations.SerializedName -import com.meloda.fast.api.model.attachments.VkMiniApp -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkMiniApp( - val title: String, - val description: String, - val app: App, - val images: List?, - val button_text: String -) : Parcelable { - - @Parcelize - data class App( - val type: String, - val id: Int, - val title: String, - @SerializedName("author_owner_id") - val authorOwnerId: Int, - @SerializedName("are_notifications_enabled") - val areNotificationsEnabled: Boolean, - @SerializedName("is_favorite") - val isFavorite: Boolean, - @SerializedName("is_installed") - val isInstalled: Boolean, - @SerializedName("track_code") - val trackCode: String, - @SerializedName("share_url") - val shareUrl: String, - @SerializedName("webview_url") - val webViewUrl: String, - @SerializedName("hide_tabbar") - val hideTabBar: Int, - @SerializedName("icon_75") - val icon75: String?, - @SerializedName("icon_139") - val icon139: String?, - @SerializedName("icon_150") - val icon150: String?, - @SerializedName("icon_278") - val icon278: String?, - @SerializedName("icon_576") - val icon576: String?, - @SerializedName("open_in_external_browser") - val openInExternalBrowser: Boolean, - @SerializedName("need_policy_confirmation") - val needPolicyConfirmation: Boolean, - @SerializedName("is_vkui_internal") - val isVkUiInternal: Boolean, - @SerializedName("has_vk_connect") - val hasVkConnect: Boolean, - @SerializedName("need_show_bottom_menu_tooltip_on_close") - val needShowBottomMenuTooltipOnClose: Boolean - ) : Parcelable - - @Parcelize - data class Image( - val height: Int, - val width: Int, - val url: String - ) : Parcelable - - fun asVkMiniApp() = VkMiniApp(link = app.shareUrl) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPhoto.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPhoto.kt deleted file mode 100644 index babe40e4..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPhoto.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.meloda.fast.api.model.base.attachments - -import android.os.Parcelable -import com.meloda.fast.api.model.attachments.VkPhoto -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkPhoto( - val album_id: Int, - val date: Int, - val id: Int, - val owner_id: Int, - val has_tags: Boolean, - val access_key: String?, - val sizes: List, - val text: String?, - val user_id: Int?, - val lat: Double?, - val long: Double?, - val post_id: Int? -) : BaseVkAttachment() { - - fun asVkPhoto() = VkPhoto( - albumId = album_id, - date = date, - id = id, - ownerId = owner_id, - hasTags = has_tags, - accessKey = access_key, - sizes = sizes, - text = text, - userId = user_id - ) - - @Parcelize - data class Size( - val height: Int, - val width: Int, - val type: String, - val url: String - ) : Parcelable - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPoll.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPoll.kt deleted file mode 100644 index 521145bb..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPoll.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.meloda.fast.api.model.base.attachments - -import android.os.Parcelable -import com.meloda.fast.api.model.attachments.VkPoll -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkPoll( - val multiple: Boolean, - val id: Int, - val votes: Int, - val anonymous: Boolean, - val closed: Boolean, - val end_date: Int, - val is_board: Boolean, - val can_vote: Boolean, - val can_edit: Boolean, - val can_report: Boolean, - val can_share: Boolean, - val created: Int, - val owner_id: Int, - val question: String, - val disable_unvote: Boolean, - val friends: List?, - val embed_hash: String, - val answers: List, - val author_id: Int, - val background: Background? -) : Parcelable { - - @Parcelize - data class Friend( - val id: Int - ) : Parcelable - - @Parcelize - data class Answer( - val id: Int, - val rate: Double, - val text: String, - val votes: Int - ) : Parcelable - - @Parcelize - data class Background( - val angle: Int, - val color: String, - val id: Int, - val name: String, - val type: String, - val points: List - ) : Parcelable { - - @Parcelize - data class Point( - val color: String, - val position: Double - ) : Parcelable - } - - fun asVkPoll() = VkPoll(id = id) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkSticker.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkSticker.kt deleted file mode 100644 index 79e053d1..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkSticker.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.meloda.fast.api.model.base.attachments - -import android.os.Parcelable -import com.meloda.fast.api.model.attachments.VkSticker -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkSticker( - val product_id: Int, - val sticker_id: Int, - val images: List, - val images_with_background: List, - val animation_url: String?, - val animations: List? -) : Parcelable { - - fun asVkSticker() = VkSticker( - id = sticker_id, - productId = product_id, - images = images, - backgroundImages = images_with_background - ) - - @Parcelize - data class Image( - val width: Int, - val height: Int, - val url: String - ) : Parcelable - - @Parcelize - data class Animation( - val type: String, - val url: String - ) : Parcelable - -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkStory.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkStory.kt deleted file mode 100644 index d16f2d51..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkStory.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.meloda.fast.api.model.base.attachments - -import android.os.Parcelable -import com.meloda.fast.api.model.attachments.VkStory -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkStory( - val id: Int, - val owner_id: Int, - val access_key: String, - val can_comment: Int, - val can_reply: Int, - val can_like: Boolean, - val can_share: Int, - val can_hide: Int, - val date: Int, - val expires_at: Int, - val is_ads: Boolean, - val photo: BaseVkPhoto?, - val replies: Replies, - val is_one_time: Boolean, - val track_code: String, - val type: String, - val views: Int, - val likes_count: Int, - val reaction_set_id: String, - val is_restricted: Boolean, - val no_sound: Boolean, - val need_mute: Boolean, - val mute_reply: Boolean, - val can_ask: Int, - val can_ask_anonymous: Int, - val preloading_enabled: Boolean, - val narratives_count: Int, - val can_use_in_narrative: Boolean -) : BaseVkAttachment() { - - fun asVkStory() = VkStory( - id = id, - ownerId = owner_id, - date = date, - photo = photo?.asVkPhoto() - ) - - @Parcelize - data class Replies( - val count: Int, - val new: Int - ) : Parcelable - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVideo.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVideo.kt deleted file mode 100644 index 0cce54de..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVideo.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.meloda.fast.api.model.base.attachments - -import android.os.Parcelable -import com.meloda.fast.api.model.attachments.VkVideo -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkVideo( - val id: Int, - val title: String, - val width: Int, - val height: Int, - val duration: Int, - val date: Int, - val comments: Int, - val description: String, - val player: String, - val added: Int, - val type: String, - val views: Int, - val can_comment: Int, - val can_edit: Int, - val can_like: Int, - val can_repost: Int, - val can_subscribe: Int, - val can_add_to_faves: Int, - val can_add: Int, - val can_attach_link: Int, - val access_key: String?, - val owner_id: Int, - val ov_id: String, - val is_favorite: Boolean, - val track_code: String, - val image: List, - val first_frame: List, - val files: File, - val timeline_thumbs: TimelineThumbs, - val ads: Ads -) : BaseVkAttachment() { - - fun asVkVideo() = VkVideo( - id = id, - ownerId = owner_id, - images = image.map { it.asVideoImage() }, - firstFrames = first_frame, - accessKey = access_key, - title = title - ) - - @Parcelize - data class Image( - val width: Int, - val height: Int, - val url: String, - val with_padding: Int? - ) : Parcelable { - - fun asVideoImage() = VkVideo.VideoImage( - width = width, - height = height, - url = url, - withPadding = with_padding == 1 - ) - } - - @Parcelize - data class FirstFrame( - val height: Int, - val width: Int, - val url: String - ) : Parcelable - - @Parcelize - data class File( - val mp4_240: String?, - val mp4_360: String?, - val mp4_480: String?, - val mp4_720: String?, - val mp4_1080: String?, - val mp4_1440: String?, - val hls: String, - val dash_uni: String, - val dash_sep: String, - val hls_ondemand: String, - val dash_ondemand: String, - val failover_host: String - ) : Parcelable - - @Parcelize - data class TimelineThumbs( - val count_per_image: Int, - val count_per_row: Int, - val count_total: Int, - val frame_height: Int, - val frame_width: Float, - val links: List, - val is_uv: Boolean, - val frequency: Int - ) : Parcelable - - @Parcelize - data class Ads( - val slot_id: Int, - val timeout: Int, - val can_play: Int, - val params: Params, - val sections: List, - val midroll_percents: List - ) : Parcelable { - - @Parcelize - data class Params( - val vk_id: Int, - val duration: Int, - val video_id: String, - val pl: Int, - val content_id: String, - val lang: Int, - val puid1: String, - val puid2: Int, - val puid3: Int, - val puid5: Int, - val puid6: Int, - val puid7: Int, - val puid9: Int, - val puid10: Int, - val puid12: Int, - val puid13: Int, - val puid14: Int, - val puid15: Int, - val puid18: Int, - val puid21: Int, - val sign: String, - val groupId: Int, - val vk_catid: Int, - val is_xz_video: Int - ) : Parcelable - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVoiceMessage.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVoiceMessage.kt deleted file mode 100644 index 23088e26..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVoiceMessage.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.meloda.fast.api.model.base.attachments - -import android.os.Parcelable -import com.meloda.fast.api.model.attachments.VkVoiceMessage -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkVoiceMessage( - val id: Int, - val owner_id: Int, - val duration: Int, - val waveform: List, - val link_ogg: String, - val link_mp3: String, - val access_key: String, - val transcript_state: String?, - val transcript: String? -) : Parcelable { - - fun asVkVoiceMessage() = VkVoiceMessage( - id = id, - ownerId = owner_id, - duration = duration, - waveform = waveform, - linkOgg = link_ogg, - linkMp3 = link_mp3, - accessKey = access_key, - transcriptState = transcript_state, - transcript = transcript - ) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWall.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWall.kt deleted file mode 100644 index e1dee465..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWall.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.meloda.fast.api.model.base.attachments - -import android.os.Parcelable -import com.meloda.fast.api.model.attachments.VkWall -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkWall( - val id: Int, - val from_id: Int, - val to_id: Int, - val date: Int, - val text: String, - val attachments: List?, - val post_source: PostSource?, - val comments: Comments?, - val likes: Likes?, - val reposts: Reposts?, - val views: Views?, - val is_favorite: Boolean, - val donut: Donut?, - val access_key: String?, - val short_text_rate: Double -) : Parcelable { - - fun asVkWall() = VkWall( - id = id, - fromId = from_id, - toId = to_id, - date = date, - text = text, - attachments = attachments, - comments = comments?.count, - likes = likes?.count, - reposts = reposts?.count, - views = views?.count, - isFavorite = is_favorite, - accessKey = access_key - ) - - @Parcelize - data class PostSource( - val type: String, - val platform: String - ) : Parcelable - - @Parcelize - data class Comments( - val count: Int, - val can_post: Int, - val groups_can_post: Boolean - ) : Parcelable - - @Parcelize - data class Likes( - val count: Int, - val user_likes: Int, - val can_like: Int, - val can_publish: Int, - ) : Parcelable - - @Parcelize - data class Reposts( - val count: Int, - val user_reposted: Int - ) : Parcelable - - @Parcelize - data class Views( - val count: Int - ) : Parcelable - - @Parcelize - data class Donut( - val is_donut: Boolean - ) : Parcelable - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWidget.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWidget.kt deleted file mode 100644 index a6bf1006..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWidget.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.api.model.base.attachments - -import com.meloda.fast.api.model.attachments.VkWidget -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkWidget(val id: Int) : BaseVkAttachment() { - - fun asVkWidget() = VkWidget(id) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/data/BaseVkConversation.kt b/app/src/main/kotlin/com/meloda/fast/api/model/data/BaseVkConversation.kt deleted file mode 100644 index 933f778b..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/data/BaseVkConversation.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.meloda.fast.api.model.data - -import android.os.Parcelable -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.model.base.BaseVkMessage -import com.meloda.fast.api.model.base.attachments.BaseVkGroupCall -import com.meloda.fast.api.model.domain.VkConversationDomain -import kotlinx.parcelize.Parcelize - -@Parcelize -data class BaseVkConversation( - val peer: Peer, - val last_message_id: Int, - val in_read: Int, - val out_read: Int, - val in_read_cmid: Int, - val out_read_cmid: Int, - val sort_id: SortId, - val last_conversation_message_id: Int, - val is_marked_unread: Boolean, - val important: Boolean, - val push_settings: PushSettings, - val can_write: CanWrite, - val can_send_money: Boolean, - val can_receive_money: Boolean, - val chat_settings: ChatSettings?, - val call_in_progress: CallInProgress?, - val unread_count: Int?, -) : Parcelable { - - @Parcelize - data class Peer( - val id: Int, - val type: String, - val local_id: Int, - ) : Parcelable - - @Parcelize - data class SortId( - val major_id: Int, - val minor_id: Int, - ) : Parcelable - - @Parcelize - data class PushSettings( - val disabled_forever: Boolean, - val no_sound: Boolean, - val disabled_mentions: Boolean, - val disabled_mass_mentions: Boolean, - ) : Parcelable - - @Parcelize - data class CanWrite( - val allowed: Boolean, - ) : Parcelable - - @Parcelize - data class ChatSettings( - val owner_id: Int, - val title: String, - val state: String, - val acl: Acl, - val members_count: Int, - val friends_count: Int, - val photo: Photo?, - val admin_ids: List, - val active_ids: List, - val is_group_channel: Boolean, - val is_disappearing: Boolean, - val is_service: Boolean, - val theme: String?, - val pinned_message: BaseVkMessage?, - ) : Parcelable { - - @Parcelize - data class Acl( - val can_change_info: Boolean, - val can_change_invite_link: Boolean, - val can_change_pin: Boolean, - val can_invite: Boolean, - val can_promote_users: Boolean, - val can_see_invite_link: Boolean, - val can_moderate: Boolean, - val can_copy_chat: Boolean, - val can_call: Boolean, - val can_use_mass_mentions: Boolean, - val can_change_style: Boolean, - ) : Parcelable - - @Parcelize - data class Photo( - val photo_50: String?, - val photo_100: String?, - val photo_200: String?, - val is_default_photo: Boolean, - ) : Parcelable - } - - @Parcelize - data class CallInProgress( - val participants: BaseVkGroupCall.Participants, - val join_link: String, - ) : Parcelable { - - @Parcelize - data class Participants( - val list: List, - val count: Int, - ) : Parcelable - - } - - fun mapToDomain( - lastMessage: VkMessage? = null, - conversationUser: VkUser? = null, - conversationGroup: VkGroup? = null, - ) = VkConversationDomain( - id = peer.id, - localId = peer.local_id, - conversationTitle = chat_settings?.title, - conversationPhoto = chat_settings?.photo?.photo_200, - type = peer.type, - isCallInProgress = call_in_progress != null, - isPhantom = chat_settings?.is_disappearing == true, - lastConversationMessageId = last_conversation_message_id, - inRead = in_read, - outRead = out_read, - lastMessageId = last_message_id, - unreadCount = unread_count ?: 0, - membersCount = chat_settings?.members_count, - ownerId = chat_settings?.owner_id, - majorId = sort_id.major_id, - minorId = sort_id.minor_id, - canChangePin = chat_settings?.acl?.can_change_pin == true, - canChangeInfo = chat_settings?.acl?.can_change_info == true, - pinnedMessageId = chat_settings?.pinned_message?.id, - inReadCmId = in_read_cmid, - outReadCmId = out_read_cmid, - ).also { - it.lastMessage = lastMessage - it.pinnedMessage = chat_settings?.pinned_message?.asVkMessage() - it.conversationUser = conversationUser - it.conversationGroup = conversationGroup - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/domain/VkConversationDomain.kt b/app/src/main/kotlin/com/meloda/fast/api/model/domain/VkConversationDomain.kt deleted file mode 100644 index 89f4d731..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/domain/VkConversationDomain.kt +++ /dev/null @@ -1,245 +0,0 @@ -package com.meloda.fast.api.model.domain - -import android.os.Parcelable -import androidx.room.Entity -import androidx.room.Ignore -import androidx.room.PrimaryKey -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.VkUtils -import com.meloda.fast.api.model.ActionState -import com.meloda.fast.api.model.ConversationPeerType -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.model.presentation.VkConversationUi -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.ext.isFalse -import com.meloda.fast.ext.isTrue -import com.meloda.fast.ext.orDots -import com.meloda.fast.model.base.UiImage -import com.meloda.fast.model.base.UiText -import com.meloda.fast.model.base.parseString -import com.meloda.fast.util.TimeUtils -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize -import java.util.Calendar - -@Suppress("MemberVisibilityCanBePrivate") -@Entity(tableName = "conversations") -@Parcelize -data class VkConversationDomain( - @PrimaryKey(autoGenerate = false) - val id: Int, - val localId: Int, - val ownerId: Int?, - val conversationTitle: String?, - val conversationPhoto: String?, - val isCallInProgress: Boolean, - val isPhantom: Boolean, - val lastConversationMessageId: Int, - val inReadCmId: Int, - val outReadCmId: Int, - val inRead: Int, - val outRead: Int, - val lastMessageId: Int, - val unreadCount: Int, - val membersCount: Int?, - val canChangePin: Boolean, - val canChangeInfo: Boolean, - val majorId: Int, - val minorId: Int, - val pinnedMessageId: Int?, - val type: String, -) : Parcelable { - - @Ignore - @IgnoredOnParcel - var peerType: ConversationPeerType = ConversationPeerType.parse(type) - - @Ignore - @IgnoredOnParcel - var lastMessage: VkMessage? = null - - @Ignore - @IgnoredOnParcel - var pinnedMessage: VkMessage? = null - - @Ignore - @IgnoredOnParcel - var conversationUser: VkUser? = null - - @Ignore - @IgnoredOnParcel - var conversationGroup: VkGroup? = null - - fun isChat() = peerType.isChat() - fun isUser() = peerType.isUser() - fun isGroup() = peerType.isGroup() - - fun isInUnread() = inRead - lastMessageId < 0 - fun isOutUnread() = outRead - lastMessageId < 0 - - fun isUnread() = isInUnread() || isOutUnread() - - fun isAccount() = id == UserConfig.userId - - fun isPinned() = majorId > 0 - - fun extractAvatar(): UiImage { - val placeholderImage = UiImage.Resource(R.drawable.ic_account_circle_cut) - - val avatarLink = when { - peerType.isUser() -> { - if (id == UserConfig.userId) { - null - } else { - conversationUser?.photo200 - } - } - - peerType.isGroup() -> conversationGroup?.photo200 - peerType.isChat() -> conversationPhoto - else -> null - } - - return avatarLink?.let(UiImage::Url) ?: placeholderImage - } - - fun extractTitle(): UiText { - return when { - isAccount() -> UiText.Resource(R.string.favorites) - peerType.isChat() -> UiText.Simple(conversationTitle ?: "...") - peerType.isUser() -> UiText.Simple(conversationUser?.fullName ?: "...") - peerType.isGroup() -> UiText.Simple(conversationGroup?.name ?: "...") - else -> UiText.Simple("...") - } - } - - fun extractUnreadCounterText(): String? { - if (lastMessage?.isOut.isFalse && !isInUnread()) return null - - return when (unreadCount) { - in 1..999 -> unreadCount.toString() - 0 -> null - else -> "%dK".format(unreadCount / 1000) - } - } - - // TODO: 07.01.2023, Danil Nikolaev: rewrite - fun extractMessage(): String { - val actionMessage = VkUtils.getActionConversationText( - message = lastMessage, - youPrefix = "You", - messageUser = lastMessage?.user, - messageGroup = lastMessage?.group, - action = lastMessage?.getPreparedAction(), - actionUser = lastMessage?.actionUser, - actionGroup = lastMessage?.actionGroup - ) - - val attachmentIcon: UiImage? = when { - lastMessage?.text == null -> null - !lastMessage?.forwards.isNullOrEmpty() -> { - if (lastMessage?.forwards?.size == 1) { - UiImage.Resource(R.drawable.ic_attachment_forwarded_message) - } else { - UiImage.Resource(R.drawable.ic_attachment_forwarded_messages) - } - } - - else -> VkUtils.getAttachmentConversationIcon(lastMessage) - } - - val attachmentText = (if (attachmentIcon == null) VkUtils.getAttachmentText( - message = lastMessage - ) else null) - - val forwardsMessage = (if (lastMessage?.text == null) VkUtils.getForwardsText( - message = lastMessage - ) else null) - - val messageText = lastMessage?.text?.let(UiText::Simple) - - var prefix = when { - actionMessage != null -> "" - lastMessage?.isOut.isTrue -> "You: " - else -> - when { - lastMessage?.user != null && lastMessage?.user?.firstName?.isNotBlank().isTrue -> { - "${lastMessage?.user?.firstName}: " - } - - lastMessage?.group != null && lastMessage?.group?.name?.isNotBlank().isTrue -> { - "${lastMessage?.group?.name}: " - } - - else -> "" - } - } - - if ((!peerType.isChat() && lastMessage?.isOut.isFalse) || id == UserConfig.userId) - prefix = "" - - val finalText = - (actionMessage ?: forwardsMessage ?: attachmentText ?: messageText) - ?.parseString(AppGlobal.Instance) - ?.let(VkUtils::prepareMessageText) - ?.let { text -> "$prefix$text" } - - - return finalText.orDots() - } - - fun extractAttachmentImage(): UiImage? { - if (lastMessage?.text == null) return null - return VkUtils.getAttachmentConversationIcon(lastMessage) - } - - fun extractReadCondition(): Boolean { - return (lastMessage?.isOut.isTrue && isOutUnread()) || - (lastMessage?.isOut.isFalse && isInUnread()) - } - - fun extractDate(): String { - return TimeUtils.getLocalizedTime(AppGlobal.Instance, (lastMessage?.date ?: -1) * 1000L) - } - - // TODO: 05.08.2023, Danil Nikolaev: rewrite - fun extractBirthday(): Boolean { - val birthday = conversationUser?.birthday ?: return false - val splitBirthday = birthday.split(".") - - return if (splitBirthday.size > 1) { - val birthdayCalendar = Calendar.getInstance().apply { - this[Calendar.DAY_OF_MONTH] = splitBirthday.first().toIntOrNull() ?: -1 - this[Calendar.MONTH] = (splitBirthday[1].toIntOrNull() ?: 0) - 1 - } - val nowCalendar = Calendar.getInstance() - - (nowCalendar[Calendar.DAY_OF_MONTH] == birthdayCalendar[Calendar.DAY_OF_MONTH] - && nowCalendar[Calendar.MONTH] == birthdayCalendar[Calendar.MONTH]) - } else false - } - - fun mapToPresentation() = VkConversationUi( - conversationId = id, - lastMessageId = lastMessageId, - avatar = extractAvatar(), - title = extractTitle(), - unreadCount = extractUnreadCounterText(), - date = extractDate(), - message = extractMessage(), - attachmentImage = extractAttachmentImage(), - isPinned = majorId > 0, - actionState = ActionState.parse(isPhantom, isCallInProgress), - isBirthday = extractBirthday(), - isUnread = extractReadCondition(), - isAccount = isAccount(), - isOnline = !isAccount() && conversationUser?.online == true, - lastMessage = lastMessage, - conversationUser = conversationUser, - conversationGroup = conversationGroup, - peerType = peerType - ) -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/presentation/VkConversationUi.kt b/app/src/main/kotlin/com/meloda/fast/api/model/presentation/VkConversationUi.kt deleted file mode 100644 index 2f731153..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/presentation/VkConversationUi.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.meloda.fast.api.model.presentation - -import com.meloda.fast.api.model.ActionState -import com.meloda.fast.api.model.ConversationPeerType -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.model.base.AdapterDiffItem -import com.meloda.fast.model.base.UiImage -import com.meloda.fast.model.base.UiText - -data class VkConversationUi( - val conversationId: Int, - val lastMessageId: Int, - val avatar: UiImage, - val title: UiText, - val unreadCount: String?, - val date: String, - val message: String, - val attachmentImage: UiImage?, - val isPinned: Boolean, - val actionState: ActionState, - val isBirthday: Boolean, - val isUnread: Boolean, - val isAccount: Boolean, - val isOnline: Boolean, - val lastMessage: VkMessage?, - val conversationUser: VkUser?, - val conversationGroup: VkGroup?, - val peerType: ConversationPeerType, -) : AdapterDiffItem { - override val id = conversationId -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/ApiErrors.kt b/app/src/main/kotlin/com/meloda/fast/api/network/ApiErrors.kt deleted file mode 100644 index 057ebb6c..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/ApiErrors.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.meloda.fast.api.network - -import com.google.gson.annotations.SerializedName -import com.meloda.fast.api.base.ApiError - -@Suppress("unused") -object VkErrorCodes { - const val UnknownError = 1 - const val AppDisabled = 2 - const val UnknownMethod = 3 - const val InvalidSignature = 4 - const val UserAuthorizationFailed = 5 - const val TooManyRequests = 6 - const val NoRights = 7 - const val BadRequest = 8 - const val TooManySimilarActions = 9 - const val InternalServerError = 10 - const val InTestMode = 11 - const val ExecuteCodeCompileError = 12 - const val ExecuteCodeRuntimeError = 13 - const val CaptchaNeeded = 14 - const val AccessDenied = 15 - const val RequiresRequestsOverHttps = 16 - const val ValidationRequired = 17 - const val UserBannedOrDeleted = 18 - const val ActionProhibited = 20 - const val ActionAllowedOnlyForStandalone = 21 - const val MethodOff = 23 - const val ConfirmationRequired = 24 - const val ParameterIsNotSpecified = 100 - const val IncorrectAppId = 101 - const val OutOfLimits = 103 - const val IncorrectUserId = 113 - const val IncorrectTimestamp = 150 - const val AccessToAlbumDenied = 200 - const val AccessToAudioDenied = 201 - const val AccessToGroupDenied = 203 - const val AlbumIsFull = 300 - const val ActionDenied = 500 - const val PermissionDenied = 600 - const val CannotSendMessageBlackList = 900 - const val CannotSendMessageGroup = 901 - const val InvalidDocId = 1150 - const val InvalidDocTitle = 1152 - const val AccessToDocDenied = 1153 - - const val AccessTokenExpired = 1117 -} - -object VkErrors { - const val Unknown = "unknown_error" - - const val NeedValidation = "need_validation" - const val NeedCaptcha = "need_captcha" - const val InvalidRequest = "invalid_request" - -} - -object VkErrorTypes { - const val OtpFormatIncorrect = "otp_format_is_incorrect" - const val WrongOtp = "wrong_otp" -} - -object VkErrorMessages { - const val UserBanned = "user has been banned" -} - -open class AuthorizationError : ApiError() - -class TokenExpiredError : AuthorizationError() - -data class ValidationRequiredError( - @SerializedName("validation_type") - val validationType: String, - @SerializedName("validation_sid") - val validationSid: String, - @SerializedName("phone_mask") - val phoneMask: String, - @SerializedName("redirect_uri") - val redirectUri: String, - @SerializedName("validation_resend") - val validationResend: String -) : ApiError() - -data class CaptchaRequiredError( - @SerializedName("captcha_sid") - val captchaSid: String, - @SerializedName("captcha_img") - val captchaImg: String -) : ApiError() - -object WrongTwoFaCodeFormatError : ApiError() - -object WrongTwoFaCodeError : ApiError() - -data class UserBannedError( - @SerializedName("ban_info") - val banInfo: BanInfo -) : ApiError() { - - data class BanInfo( - @SerializedName("member_name") - val memberName: String, - val message: String, - @SerializedName("access_token") - val accessToken: String, - @SerializedName("restore_url") - val restoreUrl: String - ) -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt b/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt deleted file mode 100644 index 32c4ca2f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.meloda.fast.api.network - -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.VKConstants -import com.meloda.fast.api.network.account.AccountUrls -import com.meloda.fast.api.network.ota.OtaUrls -import okhttp3.Interceptor -import okhttp3.Response -import java.net.URLEncoder - -class AuthInterceptor : Interceptor { - - override fun intercept(chain: Interceptor.Chain): Response { - val builder = chain.request().url.newBuilder() - - val url = builder.build().toUrl().toString() - - if (!url.contains("upload.php") && !url.contains(OtaUrls.GetActualUrl)) { - builder.addQueryParameter("v", URLEncoder.encode(VKConstants.API_VERSION, "utf-8")) - } - - if (!url.contains(AccountUrls.SetOnline) && !url.contains("upload.php")) { - UserConfig.accessToken.let { - if (it.isNotBlank()) - builder.addQueryParameter("access_token", URLEncoder.encode(it, "utf-8")) - } - } - - return chain.proceed(chain.request().newBuilder().apply { url(builder.build()) }.build()) - - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/ResultCallFactory.kt b/app/src/main/kotlin/com/meloda/fast/api/network/ResultCallFactory.kt deleted file mode 100644 index 2cde43e2..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/ResultCallFactory.kt +++ /dev/null @@ -1,155 +0,0 @@ -@file:Suppress("UNCHECKED_CAST") - -package com.meloda.fast.api.network - -import com.google.gson.Gson -import com.meloda.fast.api.VkUtils -import com.meloda.fast.api.base.ApiError -import com.meloda.fast.api.base.ApiResponse -import okhttp3.Request -import okio.Timeout -import retrofit2.* -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.contract - -class ResultCallFactory(private val gson: Gson) : CallAdapter.Factory() { - override fun get( - returnType: Type, - annotations: Array, - retrofit: Retrofit, - ): CallAdapter<*, *>? { - val rawReturnType: Class<*> = getRawType(returnType) - if (rawReturnType == Call::class.java) { - if (returnType is ParameterizedType) { - val callInnerType: Type = getParameterUpperBound(0, returnType) - if (getRawType(callInnerType) == ApiAnswer::class.java) { - if (callInnerType is ParameterizedType) { - val resultInnerType = getParameterUpperBound(0, callInnerType) - return ResultCallAdapter(resultInnerType, gson) - } - return ResultCallAdapter(Nothing::class.java, gson) - } - } - } - return null - } -} - -internal abstract class CallDelegate(protected val proxy: Call) : Call { - - override fun execute(): Response = throw NotImplementedError() - - final override fun enqueue(callback: Callback) = enqueueImpl(callback) - - final override fun clone(): Call = cloneImpl() - - override fun cancel() = proxy.cancel() - - override fun request(): Request = proxy.request() - - override fun isExecuted() = proxy.isExecuted - - override fun isCanceled() = proxy.isCanceled - - abstract fun enqueueImpl(callback: Callback) - - abstract fun cloneImpl(): Call -} - -private class ResultCallAdapter(private val type: Type, private val gson: Gson) : CallAdapter>> { - - override fun responseType() = type - - override fun adapt(call: Call): Call> = ResultCall(call, gson) -} - -internal class ResultCall(proxy: Call, private val gson: Gson) : CallDelegate>(proxy) { - - override fun enqueueImpl(callback: Callback>) { - proxy.enqueue(ResultCallback(this, callback, gson)) - } - - override fun cloneImpl(): ResultCall { - return ResultCall(proxy.clone(), gson) - } - - private class ResultCallback( - private val proxy: ResultCall, - private val callback: Callback>, - private val gson: Gson - ) : Callback { - - override fun onResponse(call: Call, response: Response) { - val result: ApiAnswer = - if (response.isSuccessful) { - val baseBody = response.body() - if (baseBody !is ApiResponse<*>) { - ApiAnswer.Success(baseBody as T) - } else { - val body = baseBody as? ApiResponse<*> - if (body?.error != null) { - VkUtils.getApiError(gson, gson.toJson(body.error)) - } else { - ApiAnswer.Success(body as T) - } - } - } else { - val errorBodyString = response.errorBody()?.string() - - VkUtils.getApiError(gson, errorBodyString) - } - - if (checkErrors(call, result)) { - return - } - - callback.onResponse(proxy, Response.success(result)) - } - - override fun onFailure(call: Call, error: Throwable) { - callback.onResponse( - proxy, - Response.success(ApiAnswer.Error(ApiError(throwable = error))) - ) - } - - private fun checkErrors(call: Call, result: ApiAnswer<*>): Boolean { - if (result.isError()) { - result.error.throwable?.run { - onFailure(call, this) - return true - } - } - - return false - } - } - - override fun timeout(): Timeout { - return proxy.timeout() - } -} - -sealed class ApiAnswer { - - data class Success(val data: T) : ApiAnswer() - data class Error(val error: ApiError) : ApiAnswer() - - @OptIn(ExperimentalContracts::class) - fun isSuccessful(): Boolean { - contract { - returns(true) implies (this@ApiAnswer is Success) - } - return this is Success - } - - @OptIn(ExperimentalContracts::class) - fun isError(): Boolean { - contract { - returns(true) implies (this@ApiAnswer is Error) - } - return this is Error - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt deleted file mode 100644 index a38e540e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.api.network - -object VkUrls { - - const val OAUTH = "https://oauth.vk.com" - const val API = "https://api.vk.com/method" -} - - diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/account/AccountUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/account/AccountUrls.kt deleted file mode 100644 index c3588a2c..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/account/AccountUrls.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.meloda.fast.api.network.account - -import com.meloda.fast.api.network.VkUrls - -object AccountUrls { - - const val SetOnline = "${VkUrls.API}/account.setOnline" - const val SetOffline = "${VkUrls.API}/account.setOffline" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosRequests.kt b/app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosRequests.kt deleted file mode 100644 index dfa5fbf3..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosRequests.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.meloda.fast.api.network.audio - diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosResponses.kt b/app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosResponses.kt deleted file mode 100644 index a0a39180..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosResponses.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.meloda.fast.api.network.audio - -import android.os.Parcelable -import com.google.gson.annotations.SerializedName -import kotlinx.parcelize.Parcelize - -@Parcelize -data class AudiosGetUploadServerResponse( - @SerializedName("upload_url") - val uploadUrl: String -) : Parcelable - -@Parcelize -data class AudiosUploadResponse( - val redirect: String, - val server: Int, - val audio: String?, - val hash: String, - val error: String? -) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosUrls.kt deleted file mode 100644 index 094f32fa..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/audio/AudiosUrls.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.api.network.audio - -import com.meloda.fast.api.network.VkUrls - -object AudiosUrls { - - const val GetUploadServer = "${VkUrls.API}/audio.getUploadServer" - - const val Save = "${VkUrls.API}/audio.save" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthResponse.kt deleted file mode 100644 index f206f415..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthResponse.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.api.network.auth - -import android.os.Parcelable -import com.google.gson.annotations.SerializedName -import kotlinx.parcelize.Parcelize - -@Parcelize -data class AuthDirectResponse( - @SerializedName("access_token") val accessToken: String?, - @SerializedName("user_id") val userId: Int?, - @SerializedName("trusted_hash") val twoFaHash: String?, - @SerializedName("validation_sid") val validationSid: String?, - @SerializedName("validation_type") val validationType: String?, - @SerializedName("phone_mask") val phoneMask: String?, - @SerializedName("redirect_uri") val redirectUrl: String?, - @SerializedName("validation_resend") val validationResend: String?, - @SerializedName("cant_get_code_open_restore") val isCanNotGetCodeNeedToOpenRestore: Boolean -) : Parcelable - -@Parcelize -data class SendSmsResponse( - @SerializedName("sid") val validationSid: String?, - @SerializedName("delay") val delay: Int?, - @SerializedName("validation_type") val validationType: String?, - @SerializedName("validation_resend") val validationResend: String? -) : Parcelable diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthUrls.kt deleted file mode 100644 index 1a888435..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthUrls.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.meloda.fast.api.network.auth - -import com.meloda.fast.api.network.VkUrls - -object AuthUrls { - - const val DirectAuth = "${VkUrls.OAUTH}/token" - const val SendSms = "${VkUrls.API}/auth.validatePhone" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsResponse.kt deleted file mode 100644 index c8f461a4..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsResponse.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.api.network.conversations - -import android.os.Parcelable -import com.google.gson.annotations.SerializedName -import com.meloda.fast.api.model.data.BaseVkConversation -import com.meloda.fast.api.model.base.BaseVkGroup -import com.meloda.fast.api.model.base.BaseVkMessage -import com.meloda.fast.api.model.base.BaseVkUser -import kotlinx.parcelize.Parcelize - -@Parcelize -data class ConversationsGetResponse( - val count: Int, - val items: List, - @SerializedName("unread_count") - val unreadCount: Int?, - val profiles: List?, - val groups: List? -) : Parcelable - -@Parcelize -data class ConversationsResponseItems( - val conversation: BaseVkConversation, - @SerializedName("last_message") - val lastMessage: BaseVkMessage? -) : Parcelable diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsUrls.kt deleted file mode 100644 index 17715d2f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsUrls.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.meloda.fast.api.network.conversations - -import com.meloda.fast.api.network.VkUrls - -object ConversationsUrls { - - const val Get = "${VkUrls.API}/messages.getConversations" - const val Delete = "${VkUrls.API}/messages.deleteConversation" - const val Pin = "${VkUrls.API}/messages.pinConversation" - const val Unpin = "${VkUrls.API}/messages.unpinConversation" - const val ReorderPinned = "${VkUrls.API}/messages.reorderPinnedConversations" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/files/FileRequests.kt b/app/src/main/kotlin/com/meloda/fast/api/network/files/FileRequests.kt deleted file mode 100644 index a94949af..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/files/FileRequests.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.meloda.fast.api.network.files - diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/files/FilesResponses.kt b/app/src/main/kotlin/com/meloda/fast/api/network/files/FilesResponses.kt deleted file mode 100644 index abef98ae..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/files/FilesResponses.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.meloda.fast.api.network.files - -import android.os.Parcelable -import com.google.gson.annotations.SerializedName -import com.meloda.fast.api.model.base.attachments.BaseVkFile -import com.meloda.fast.api.model.base.attachments.BaseVkVoiceMessage -import kotlinx.parcelize.Parcelize - -@Parcelize -data class FilesGetMessagesUploadServerResponse( - @SerializedName("upload_url") - val uploadUrl: String -) : Parcelable - -@Parcelize -data class FilesUploadFileResponse(val file: String?, val error: String?) : Parcelable - -@Parcelize -data class FilesSaveFileResponse( - val type: String, - @SerializedName("doc") - val file: BaseVkFile?, - @SerializedName("audio_message") - val voiceMessage: BaseVkVoiceMessage? -) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/files/FilesUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/files/FilesUrls.kt deleted file mode 100644 index 1282e234..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/files/FilesUrls.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.api.network.files - -import com.meloda.fast.api.network.VkUrls - -object FilesUrls { - - const val GetMessagesUploadServer = "${VkUrls.API}/docs.getMessagesUploadServer" - - const val Save = "${VkUrls.API}/docs.save" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesResponse.kt deleted file mode 100644 index 86dd9a6a..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesResponse.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.meloda.fast.api.network.messages - -import android.os.Parcelable -import com.meloda.fast.api.model.base.* -import com.meloda.fast.api.model.data.BaseVkConversation -import kotlinx.parcelize.Parcelize - -@Parcelize -data class MessagesGetHistoryResponse( - val count: Int, - val items: List = emptyList(), - val conversations: List?, - val profiles: List?, - val groups: List? -) : Parcelable - -@Parcelize -data class MessagesGetByIdResponse( - val count: Int, - val items: List = emptyList(), - val profiles: List?, - val groups: List? -) : Parcelable - -@Parcelize -data class MessagesGetConversationMembersResponse( - val count: Int, - val items: List = emptyList(), - val profiles: List?, - val groups: List? -) : Parcelable diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesUrls.kt deleted file mode 100644 index 829c76c6..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesUrls.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.meloda.fast.api.network.messages - -import com.meloda.fast.api.network.VkUrls - -object MessagesUrls { - - const val GetHistory = "${VkUrls.API}/messages.getHistory" - const val Send = "${VkUrls.API}/messages.send" - const val MarkAsImportant = "${VkUrls.API}/messages.markAsImportant" - const val GetLongPollServer = "${VkUrls.API}/messages.getLongPollServer" - const val GetLongPollHistory = "${VkUrls.API}/messages.getLongPollHistory" - const val Pin = "${VkUrls.API}/messages.pin" - const val Unpin = "${VkUrls.API}/messages.unpin" - const val Delete = "${VkUrls.API}/messages.delete" - const val Edit = "${VkUrls.API}/messages.edit" - const val GetById = "${VkUrls.API}/messages.getById" - const val MarkAsRead = "${VkUrls.API}/messages.markAsRead" - const val GetChat = "${VkUrls.API}/messages.getChat" - const val GetConversationMembers = "${VkUrls.API}/messages.getConversationMembers" - const val RemoveChatUser = "${VkUrls.API}/messages.removeChatUser" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/ota/OtaResponses.kt b/app/src/main/kotlin/com/meloda/fast/api/network/ota/OtaResponses.kt deleted file mode 100644 index fd0c6961..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/ota/OtaResponses.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.meloda.fast.api.network.ota - -import android.os.Parcelable -import com.meloda.fast.model.UpdateItem -import kotlinx.parcelize.Parcelize - -@Parcelize -data class OtaGetLatestReleaseResponse(val release: UpdateItem?) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/ota/OtaUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/ota/OtaUrls.kt deleted file mode 100644 index 473d0ea9..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/ota/OtaUrls.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.meloda.fast.api.network.ota - -object OtaUrls { - - const val GetActualUrl = - "https://raw.githubusercontent.com/melod1n/ota-server/master/ngrok_url.json" -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotoUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotoUrls.kt deleted file mode 100644 index 05b897e5..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotoUrls.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.api.network.photos - -import com.meloda.fast.api.network.VkUrls - -object PhotoUrls { - - const val GetMessagesUploadServer = "${VkUrls.API}/photos.getMessagesUploadServer" - - const val SaveMessagePhoto = "${VkUrls.API}/photos.saveMessagesPhoto" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotosRequests.kt b/app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotosRequests.kt deleted file mode 100644 index 8e457ff0..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotosRequests.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.meloda.fast.api.network.photos - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class PhotosSaveMessagePhotoRequest( - val photo: String, val server: Int, val hash: String -) : Parcelable { - val map - get() = mapOf( - "photo" to photo, - "server" to server.toString(), - "hash" to hash - ) -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotosResponses.kt b/app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotosResponses.kt deleted file mode 100644 index c9a31973..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/photos/PhotosResponses.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.meloda.fast.api.network.photos - -import android.os.Parcelable -import com.google.gson.annotations.SerializedName -import kotlinx.parcelize.Parcelize - -@Parcelize -data class PhotosGetMessagesUploadServerResponse( - @SerializedName("album_id") - val albumId: Int, - @SerializedName("upload_url") - val uploadUrl: String -) : Parcelable - -@Parcelize -data class PhotosUploadPhotoResponse( - val server: Int, val photo: String, val hash: String -) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersResponse.kt deleted file mode 100644 index c4084e58..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersResponse.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.meloda.fast.api.network.users - diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersUrls.kt deleted file mode 100644 index 64761573..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersUrls.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.api.network.users - -import com.meloda.fast.api.network.VkUrls - -object UsersUrls { - - const val GetById = "${VkUrls.API}/users.get" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosRequests.kt b/app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosRequests.kt deleted file mode 100644 index 1f196662..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosRequests.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.meloda.fast.api.network.videos - diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosResponses.kt b/app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosResponses.kt deleted file mode 100644 index 86264318..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosResponses.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.meloda.fast.api.network.videos - -import android.os.Parcelable -import com.google.gson.annotations.SerializedName -import kotlinx.parcelize.Parcelize - -@Parcelize -data class VideosSaveResponse( - @SerializedName("access_key") - val accessKey: String, - val description: String, - @SerializedName("owner_id") - val ownerId: Int, - val title: String, - @SerializedName("upload_url") - val uploadUrl: String, - @SerializedName("video_id") - val videoId: Int -) : Parcelable { - -} - -@Parcelize -data class VideosUploadResponse( - @SerializedName("video_hash") - val hash: String?, - val size: Int, - @SerializedName("direct_link") - val directLink: String, - @SerializedName("owner_id") - val ownerId: Int, - @SerializedName("video_id") - val videoId: Int, - val error: String? -) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosUrls.kt deleted file mode 100644 index c2cc9308..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/videos/VideosUrls.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.api.network.videos - -import com.meloda.fast.api.network.VkUrls - -object VideosUrls { - - const val Save = "${VkUrls.API}/video.save" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/base/BaseActivity.kt b/app/src/main/kotlin/com/meloda/fast/base/BaseActivity.kt deleted file mode 100644 index 3113c485..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/BaseActivity.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.base - -import androidx.annotation.LayoutRes -import androidx.appcompat.app.AppCompatActivity - -abstract class BaseActivity : AppCompatActivity { - - constructor() : super() - - constructor(@LayoutRes resId: Int) : super(resId) -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/BaseFragment.kt b/app/src/main/kotlin/com/meloda/fast/base/BaseFragment.kt deleted file mode 100644 index 20d217db..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/BaseFragment.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.base - -import androidx.annotation.LayoutRes -import androidx.fragment.app.Fragment - -abstract class BaseFragment : Fragment { - - constructor() : super() - - constructor(@LayoutRes resId: Int) : super(resId) -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/ResourceProvider.kt b/app/src/main/kotlin/com/meloda/fast/base/ResourceProvider.kt deleted file mode 100644 index f2ab5f99..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/ResourceProvider.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.base - -import android.content.Context -import android.graphics.drawable.Drawable -import androidx.annotation.ColorInt -import androidx.annotation.ColorRes -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.core.content.ContextCompat - -abstract class ResourceProvider(protected val context: Context) { - - protected fun getString(@StringRes resId: Int): String { - return context.getString(resId) - } - - @ColorInt - protected fun getColor(@ColorRes resId: Int): Int { - return ContextCompat.getColor(context, resId) - } - - protected fun getDrawable(@DrawableRes resId: Int): Drawable? { - return ContextCompat.getDrawable(context, resId) - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/base/adapter/AsyncDiffItemAdapter.kt b/app/src/main/kotlin/com/meloda/fast/base/adapter/AsyncDiffItemAdapter.kt deleted file mode 100644 index 9167b253..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/adapter/AsyncDiffItemAdapter.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.meloda.fast.base.adapter - -import androidx.recyclerview.widget.DiffUtil -import com.hannesdorfmann.adapterdelegates4.AdapterDelegate -import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter -import com.meloda.fast.model.base.AdapterDiffItem - -class AsyncDiffItemAdapter( - customDiffCallback: DiffUtil.ItemCallback? = null, - vararg delegates: AdapterDelegate>, -) : AsyncListDifferDelegationAdapter(customDiffCallback ?: DIFF_CALLBACK) { - - constructor( - vararg delegates: AdapterDelegate>, - ) : this(customDiffCallback = null) { - delegates.forEach(::addDelegate) - } - - init { - delegates.forEach(::addDelegate) - } - - fun addDelegates(vararg delegates: AdapterDelegate>) { - delegates.forEach(::addDelegate) - } - - @Suppress("UNCHECKED_CAST") - fun addDelegate(delegate: AdapterDelegate>) { - (delegate as? AdapterDelegate>)?.let(delegatesManager::addDelegate) - } - - fun isEmpty() = itemCount == 0 - fun isNotEmpty() = itemCount > 0 - - companion object { - val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: AdapterDiffItem, - newItem: AdapterDiffItem, - ): Boolean { - return oldItem.areItemsTheSame(newItem) - } - - override fun areContentsTheSame( - oldItem: AdapterDiffItem, - newItem: AdapterDiffItem, - ): Boolean { - return oldItem.areContentsTheSame(newItem) - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/adapter/BaseAdapter.kt b/app/src/main/kotlin/com/meloda/fast/base/adapter/BaseAdapter.kt deleted file mode 100644 index 22ac45b8..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/adapter/BaseAdapter.kt +++ /dev/null @@ -1,281 +0,0 @@ -package com.meloda.fast.base.adapter - -import android.content.Context -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.widget.AdapterView -import android.widget.Filter -import android.widget.Filterable -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import kotlinx.coroutines.* -import kotlin.properties.Delegates - -@Suppress("MemberVisibilityCanBePrivate", "unused", "UNCHECKED_CAST") -abstract class BaseAdapter constructor( - var context: Context, - diffUtil: DiffUtil.ItemCallback, - preAddedValues: List = emptyList(), -) : ListAdapter(diffUtil), Filterable { - - private var valuesFilter: ValuesFilter? = null - - protected val adapterScope = CoroutineScope(Dispatchers.Default) - private val cleanList = mutableListOf() - - protected var inflater: LayoutInflater = LayoutInflater.from(context) - - var itemClickListener: ((position: Int) -> Unit)? = null - var itemLongClickListener: ((position: Int) -> Boolean)? = null - - private val listForSave = mutableListOf() - - var isSearching: Boolean by Delegates.observable(false) { _, _, _ -> - updateSearchingState() - } - - init { - cleanList.addAll(preAddedValues) - addAll(preAddedValues) - } - - fun cloneCurrentList(): MutableList { - return currentList.toMutableList() - } - - open fun destroy() {} - - fun getOrNull(position: Int): T? { - return if (position >= 0 && position <= currentList.lastIndex) get(position) else null - } - - fun getOrElse(position: Int, defaultValue: (Int) -> T): T { - return if (position >= 0 && position <= currentList.lastIndex) get(position) - else defaultValue(position) - } - - fun add( - item: T, - position: Int? = null, - commitCallback: (() -> Unit)? = null - ) = addAll(listOf(item), position, commitCallback) - - fun addAll( - items: List, - position: Int? = null, - commitCallback: (() -> Unit)? = null - ) { - adapterScope.launch { - val newList = cloneCurrentList() - if (position == null) { - val mutableItems = items.toMutableList() - - newList.addAll(mutableItems) - cleanList.addAll(mutableItems) - } else { - newList.addAll(position, items) - cleanList.addAll(position, items) - } - - withContext(Dispatchers.Main) { - submitList(newList, commitCallback) - } - } - } - - fun remove(item: T, commitCallback: (() -> Unit)? = null) = - removeAll(listOf(item), commitCallback) - - fun removeAll(items: List, commitCallback: (() -> Unit)? = null) { - val newList = cloneCurrentList() - newList.removeAll(items) - cleanList.removeAll(items) - - submitList(newList, commitCallback) - } - - fun removeAt(index: Int, commitCallback: (() -> Unit)? = null) { - val newList = cloneCurrentList() - newList.removeAt(index) - cleanList.removeAt(index) - - submitList(newList, commitCallback) - } - - fun clear(commitCallback: (() -> Unit)? = null) = removeAll(currentList, commitCallback) - - fun setItem( - item: T, - commitCallback: (() -> Unit)? = null - ) = setItems(listOf(item), commitCallback) - - @Suppress("UNCHECKED_CAST") - fun setItems( - list: List?, - commitCallback: (() -> Unit)? = null - ) { - adapterScope.launch { - val items = mutableListOf() - if (!list.isNullOrEmpty()) items.addAll(list) - - withContext(Dispatchers.Main) { - if (items == currentList) { - refreshList() - } else { - submitList(items, commitCallback) - } - } - } - } - - fun indexOf(item: T): Int { - return currentList.indexOf(item) - } - - fun searchIndexOf(item: T): Int? { - val index = indexOf(item) - return if (index == -1) null else index - } - - val indices get() = currentList.indices - - operator fun get(position: Int): T { - return currentList[position] - } - - operator fun set(position: Int, item: T) = setItem(position, item) - - fun setItem(position: Int, item: T, commitCallback: (() -> Unit)? = null) { - val newList = cloneCurrentList() - newList[position] = item - cleanList[position] = item - - submitList(newList, commitCallback) - } - - fun isEmpty() = currentList.isEmpty() - fun isNotEmpty() = currentList.isNotEmpty() - - fun refreshList() { - notifyItemRangeChanged(0, itemCount) - } - - fun updateCleanList(list: List?) { - cleanList.clear() - list?.run { cleanList.addAll(this) } - } - - override fun submitList(list: List?) { - super.submitList(list) - updateCleanList(list) - } - - override fun submitList(list: List?, commitCallback: Runnable?) { - super.submitList(list, commitCallback) - updateCleanList(list) - } - - override fun onBindViewHolder(holder: VH, position: Int) { - initListeners(holder.itemView, position) - holder.bind(position) - } - - protected open fun initListeners(itemView: View, position: Int) { - if (itemView is AdapterView<*>) return - - itemView.setOnClickListener { itemClickListener?.invoke(position) } - itemView.setOnLongClickListener { - itemLongClickListener?.invoke(position) - return@setOnLongClickListener itemClickListener != null - } - } - - override fun getItemCount(): Int { - return currentList.size - } - - val lastPosition get() = currentList.lastIndex - - private fun updateSearchingState() { - Log.d("BaseAdapter", "updateSearchingState: $isSearching") - - cleanList.clear() - - if (isSearching) { - listForSave.clear() - listForSave += cloneCurrentList() - } else { - setItems(listForSave, commitCallback = { - listForSave.clear() - }) - } - } - - open fun filter(query: String) { - if (cleanList.isEmpty()) { - cleanList.addAll(listForSave) - } - - val newList = mutableListOf() - - setItems(emptyList(), commitCallback = { - if (query.isEmpty()) { - newList.addAll(cleanList) - } else { - for (item in cleanList) { - if (onQueryItem(item, query)) { - newList.add(item) - } - } - } - - setItems(newList) - }) - } - - open fun onQueryItem(item: T, query: String): Boolean { - return false - } - - override fun getFilter(): Filter { - if (valuesFilter == null) { - valuesFilter = ValuesFilter() - } - - return requireNotNull(valuesFilter) - } - - private inner class ValuesFilter : Filter() { - override fun performFiltering(constraint: CharSequence?): FilterResults { - val results = FilterResults() - - if (isEmpty()) return results - - if (!constraint.isNullOrEmpty()) { - val filteredList = mutableListOf() - for (item in listForSave) { - if (onQueryItem(item, constraint.toString())) { - filteredList.add(item) - } - } - results.count = filteredList.size - results.values = filteredList - } else { - results.count = listForSave.size - results.values = listForSave - } - - return results - } - - override fun publishResults(constraint: CharSequence?, results: FilterResults) { - val items = results.values as? List - setItems(items) - } - } - - override fun onCurrentListChanged(previousList: MutableList, currentList: MutableList) { - super.onCurrentListChanged(previousList, currentList) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/base/adapter/Holders.kt b/app/src/main/kotlin/com/meloda/fast/base/adapter/Holders.kt deleted file mode 100644 index df2f0d63..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/adapter/Holders.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.meloda.fast.base.adapter - -import android.view.View -import androidx.recyclerview.widget.RecyclerView - -abstract class BaseHolder(v: View) : RecyclerView.ViewHolder(v) { - - open fun bind(position: Int) { - bind(position, null) - } - - open fun bind(position: Int, payloads: MutableList?) {} - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/base/adapter/Listeners.kt b/app/src/main/kotlin/com/meloda/fast/base/adapter/Listeners.kt deleted file mode 100644 index 204ec669..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/adapter/Listeners.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.base.adapter - -fun interface OnItemClickListener { - fun onItemClick(item: T) -} - -fun interface OnItemLongClickListener { - fun onLongItemClick(item: T): Boolean -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/screen/AppScreen.kt b/app/src/main/kotlin/com/meloda/fast/base/screen/AppScreen.kt deleted file mode 100644 index 2fd269af..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/screen/AppScreen.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.meloda.fast.base.screen - -import com.github.terrakok.cicerone.Router -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.MutableSharedFlow - -interface AppScreen { - val resultFlow: MutableSharedFlow - - var args: ArgType - - fun show(router: Router, args: ArgType) - - fun getArguments(): ArgType = args -} - -@Suppress("unused") -fun AppScreen.createResultFlow(): MutableSharedFlow { - return MutableSharedFlow( - extraBufferCapacity = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt deleted file mode 100644 index 9509b8c4..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.meloda.fast.base.viewmodel - -import androidx.lifecycle.ViewModel -import com.meloda.fast.api.base.ApiError -import com.meloda.fast.api.network.* -import com.meloda.fast.ext.isTrue -import com.meloda.fast.ext.notNull - -abstract class BaseViewModel : ViewModel() { - - open suspend fun sendSingleEvent(event: VkEvent) {} - - suspend fun sendRequestNotNull( - onError: ErrorHandler? = null, - request: suspend () -> ApiAnswer - ): T = sendRequest(onError, request).notNull() - - suspend fun sendRequest( - onError: ErrorHandler? = null, - request: suspend () -> ApiAnswer, - ): T? { - return when (val response = request()) { - is ApiAnswer.Success -> response.data - is ApiAnswer.Error -> { - val error = response.error - - if (!onError?.handleError(error).isTrue) { - checkErrors(error) - } - - null - } - } - } - - protected suspend fun checkErrors(throwable: Throwable) { - when (throwable) { - is TokenExpiredError -> { - sendSingleEvent(TokenExpiredErrorEvent) - } - is AuthorizationError -> { - sendSingleEvent(AuthorizationErrorEvent) - } - is UserBannedError -> { - throwable.banInfo.let { banInfo -> - sendSingleEvent( - UserBannedEvent( - memberName = banInfo.memberName, - message = banInfo.message, - restoreUrl = banInfo.restoreUrl, - accessToken = banInfo.accessToken - ) - ) - } - } - is ValidationRequiredError -> { - sendSingleEvent( - ValidationRequiredEvent( - sid = throwable.validationSid, - redirectUri = throwable.redirectUri, - phoneMask = throwable.phoneMask, - validationType = throwable.validationType, - canResendSms = throwable.validationResend == "sms", - codeError = null - ) - ) - } - is CaptchaRequiredError -> { - sendSingleEvent( - CaptchaRequiredEvent( - sid = throwable.captchaSid, - image = throwable.captchaImg - ) - ) - } - - is ApiError -> { - sendSingleEvent( - if (throwable.errorMessage == null) { - UnknownErrorEvent - } else { - ErrorTextEvent(errorText = requireNotNull(throwable.errorMessage)) - } - ) - } - else -> { - sendSingleEvent( - if (throwable.message == null) { - UnknownErrorEvent - } else { - ErrorTextEvent(requireNotNull(throwable.message)) - } - ) - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModelFragment.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModelFragment.kt deleted file mode 100644 index cb9f8b38..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModelFragment.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.meloda.fast.base.viewmodel - -import android.os.Bundle -import android.view.View -import androidx.annotation.LayoutRes -import androidx.lifecycle.lifecycleScope -import com.meloda.fast.base.BaseFragment -import kotlinx.coroutines.launch - -@Deprecated("", ReplaceWith("BaseFragment")) -abstract class BaseViewModelFragment : BaseFragment { - - constructor() : super() - - constructor(@LayoutRes resId: Int) : super(resId) - - protected abstract val viewModel: VM - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - subscribeToViewModel(viewModel) - } - - protected open fun onEvent(event: VkEvent) { - ViewModelUtils.parseEvent(this, event) - } - - protected fun subscribeToViewModel(viewModel: T) { - lifecycleScope.launch { - viewModel.tasksEvent.collect { onEvent(it) } - } - } - -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/DeprecatedBaseViewModel.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/DeprecatedBaseViewModel.kt deleted file mode 100644 index b88206ac..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/DeprecatedBaseViewModel.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.meloda.fast.base.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.meloda.fast.api.base.ApiError -import com.meloda.fast.api.network.* -import com.meloda.fast.ext.isTrue -import com.meloda.fast.ext.notNull -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.launch - -@Deprecated("rewrite") -abstract class DeprecatedBaseViewModel : ViewModel() { - - private val tasksEventChannel = Channel() - val tasksEvent = tasksEventChannel.receiveAsFlow() - - private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> - viewModelScope.launch { onException(throwable) } - } - - fun launch(block: suspend CoroutineScope.() -> Unit): Job { - return viewModelScope.launch(exceptionHandler, block = block) - } - - suspend fun sendRequestNotNull( - onError: ErrorHandler? = null, - request: suspend () -> ApiAnswer - ): T = sendRequest(onError, request).notNull() - - suspend fun sendRequest( - onError: ErrorHandler? = null, - request: suspend () -> ApiAnswer, - ): T? { - return when (val response = request()) { - is ApiAnswer.Success -> response.data - is ApiAnswer.Error -> { - val error = response.error - - if (!onError?.handleError(error).isTrue) { - checkErrors(error) - } - - null - } - } - } - - // TODO: 05.04.2023, Danil Nikolaev: переписать makeJob на sendRequest (oh boy, писать дохуя) - // TODO: 05.04.2023, Danil Nikolaev: переписать Conversations Screen на новую архитектуру, пока что оставить View - - protected fun makeJob( - job: suspend () -> ApiAnswer, - onAnswer: suspend (T) -> Unit = {}, - onStart: (suspend () -> Unit)? = null, - onEnd: (suspend () -> Unit)? = null, - onError: (suspend (Throwable) -> Unit)? = null, - onAnyResult: (suspend () -> Unit)? = null, - ): Job = viewModelScope.launch { - onStart?.invoke() - when (val response = job()) { - is ApiAnswer.Success -> { - onAnswer(response.data) - onAnyResult?.invoke() - } - is ApiAnswer.Error -> { - onError?.invoke(response.error) ?: checkErrors(response.error) - onAnyResult?.invoke() - } - } - }.also { - it.invokeOnCompletion { - viewModelScope.launch { - onEnd?.invoke() - } - } - } - - protected open suspend fun onException(throwable: Throwable) { - checkErrors(throwable) - } - - protected suspend fun sendEvent(event: T) = tasksEventChannel.send(event) - - protected suspend fun checkErrors(throwable: Throwable) { - when (throwable) { - is TokenExpiredError -> sendEvent(TokenExpiredErrorEvent) - is AuthorizationError -> sendEvent(AuthorizationErrorEvent) - is UserBannedError -> { - val banInfo = throwable.banInfo - sendEvent( - UserBannedEvent( - memberName = banInfo.memberName, - message = banInfo.message, - restoreUrl = banInfo.restoreUrl, - accessToken = banInfo.accessToken - ) - ) - } - is ValidationRequiredError -> { - sendEvent( - ValidationRequiredEvent( - sid = throwable.validationSid, - redirectUri = throwable.redirectUri, - phoneMask = throwable.phoneMask, - validationType = throwable.validationType, - canResendSms = throwable.validationResend == "sms", - codeError = null - ) - ) - } - is CaptchaRequiredError -> sendEvent( - CaptchaRequiredEvent( - sid = throwable.captchaSid, - image = throwable.captchaImg - ) - ) - - is ApiError -> sendEvent( - if (throwable.errorMessage == null) { - UnknownErrorEvent - } else { - ErrorTextEvent(errorText = requireNotNull(throwable.errorMessage)) - } - ) - else -> sendEvent( - if (throwable.message == null) { - UnknownErrorEvent - } else { - ErrorTextEvent(requireNotNull(throwable.message)) - } - ) - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/ErrorHandler.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/ErrorHandler.kt deleted file mode 100644 index aa702965..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/ErrorHandler.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.base.viewmodel - -fun interface ErrorHandler { - - /** - * @return true if error has been handled manually - */ - suspend fun handleError(error: Throwable): Boolean -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/Events.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/Events.kt deleted file mode 100644 index 02acae53..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/Events.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.meloda.fast.base.viewmodel - -import com.meloda.fast.model.base.UiText - -abstract class VkEvent - -abstract class VkErrorEvent(open val errorText: String? = null) : VkEvent() - -object UnknownErrorEvent : VkErrorEvent() -open class ErrorTextEvent(override val errorText: String) : VkErrorEvent() - -object AuthorizationErrorEvent : VkErrorEvent() -object TokenExpiredErrorEvent : VkErrorEvent() -data class CaptchaRequiredEvent(val sid: String, val image: String) : VkErrorEvent() -data class ValidationRequiredEvent( - val sid: String, - val redirectUri: String, - val phoneMask: String, - val validationType: String, - val canResendSms: Boolean, - val codeError: UiText? -) : VkErrorEvent() - -data class UserBannedEvent( - val memberName: String, val message: String, val restoreUrl: String, val accessToken: String, -) : VkErrorEvent() - -fun interface VkEventCallback { - fun onEvent(event: T) -} diff --git a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/ViewModelUtils.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/ViewModelUtils.kt deleted file mode 100644 index 716c143f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/ViewModelUtils.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.meloda.fast.base.viewmodel - -import android.content.Intent -import android.widget.Toast -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig -import com.meloda.fast.ext.showDialog -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.main.activity.MainActivity - -object ViewModelUtils { - - @Deprecated("rewrite") - @Suppress("MemberVisibilityCanBePrivate") - fun parseEvent(activity: FragmentActivity, event: VkEvent) { - when (event) { - is AuthorizationErrorEvent -> { - Toast.makeText( - activity, R.string.authorization_failed, Toast.LENGTH_LONG - ).show() - - UserConfig.clear() - activity.finishAffinity() - activity.startActivity(Intent(activity, MainActivity::class.java)) - } - is TokenExpiredErrorEvent -> { - Toast.makeText( - activity, R.string.token_expired, Toast.LENGTH_LONG - ).show() - - UserConfig.clear() - activity.finishAffinity() - activity.startActivity(Intent(activity, MainActivity::class.java)) - } - is UserBannedEvent -> { - // TODO: 17.04.2023, Danil Nikolaev: handle banned event -// (activity as? MainActivity)?.accessRouter()?.newRootScreen( -// Screens.UserBanned( -// memberName = event.memberName, -// message = event.message, -// restoreUrl = event.restoreUrl, -// accessToken = event.accessToken -// ) -// ) - } - is UnknownErrorEvent -> { - activity.showDialog( - title = UiText.Resource(R.string.title_error), - message = UiText.Resource(R.string.unknown_error_occurred), - positiveText = UiText.Resource(R.string.ok) - ) - } - is VkErrorEvent -> { - event.errorText?.run { - activity.showDialog( - title = UiText.Resource(R.string.title_error), - message = UiText.Simple(this), - positiveText = UiText.Resource(R.string.ok) - ) - } - } - } - } - - @Deprecated("rewrite") - fun parseEvent(fragment: Fragment, event: VkEvent) { - parseEvent(fragment.requireActivity(), event) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/common/AppConstants.kt b/app/src/main/kotlin/com/meloda/fast/common/AppConstants.kt deleted file mode 100644 index 62d545a7..00000000 --- a/app/src/main/kotlin/com/meloda/fast/common/AppConstants.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.meloda.fast.common - -object AppConstants { - - const val INSTALL_APP_MIME_TYPE = "application/vnd.android.package-archive" - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/common/AppGlobal.kt b/app/src/main/kotlin/com/meloda/fast/common/AppGlobal.kt deleted file mode 100644 index e8c3b4f8..00000000 --- a/app/src/main/kotlin/com/meloda/fast/common/AppGlobal.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.meloda.fast.common - -import android.app.Application -import android.content.Context -import android.content.SharedPreferences -import android.content.pm.PackageManager -import android.content.res.Resources -import android.media.AudioManager -import androidx.appcompat.app.AppCompatDelegate -import androidx.core.content.pm.PackageInfoCompat -import androidx.preference.PreferenceManager -import com.google.android.material.color.DynamicColors -import com.meloda.fast.common.di.applicationModule -import com.meloda.fast.screens.settings.SettingsFragment -import com.meloda.fast.util.AndroidUtils -import org.koin.android.ext.koin.androidContext -import org.koin.android.ext.koin.androidLogger -import org.koin.core.context.GlobalContext.startKoin -import kotlin.math.roundToInt -import kotlin.properties.Delegates - -class AppGlobal : Application() { - - override fun onCreate() { - super.onCreate() - - instance = this - - if (preferences.getBoolean( - SettingsFragment.KEY_USE_DYNAMIC_COLORS, - SettingsFragment.DEFAULT_VALUE_USE_DYNAMIC_COLORS - ) - ) { - DynamicColors.applyToActivitiesIfAvailable(this) - } - - val info = packageManager.getPackageInfo(this.packageName, PackageManager.GET_ACTIVITIES) - versionName = info.versionName - versionCode = PackageInfoCompat.getLongVersionCode(info).toInt() - - screenWidth80 = (AndroidUtils.getDisplayWidth() * 0.8).roundToInt() - - audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager - - applyDarkTheme() - - initKoin() - } - - private fun applyDarkTheme() { - val nightMode = preferences.getInt( - SettingsFragment.KEY_APPEARANCE_DARK_THEME, - SettingsFragment.DEFAULT_VALUE_APPEARANCE_DARK_THEME - ) - AppCompatDelegate.setDefaultNightMode(nightMode) - } - - private fun initKoin() { - startKoin { - androidLogger() - androidContext(this@AppGlobal) - modules(applicationModule) - } - } - - companion object { - private lateinit var instance: AppGlobal - - val preferences: SharedPreferences by lazy { - PreferenceManager.getDefaultSharedPreferences(instance) - } - - var versionName = "" - var versionCode = 0 - var screenWidth80 = 0 - - val Instance: AppGlobal get() = instance - val resources: Resources get() = Instance.resources - val packageManager: PackageManager get() = Instance.packageManager - - var audioManager: AudioManager by Delegates.notNull() - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/common/Screens.kt b/app/src/main/kotlin/com/meloda/fast/common/Screens.kt deleted file mode 100644 index b0719a89..00000000 --- a/app/src/main/kotlin/com/meloda/fast/common/Screens.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.meloda.fast.common - -import com.github.terrakok.cicerone.androidx.FragmentScreen -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.model.domain.VkConversationDomain -import com.meloda.fast.model.UpdateItem -import com.meloda.fast.screens.chatinfo.ChatInfoFragment -import com.meloda.fast.screens.conversations.ConversationsFragment -import com.meloda.fast.screens.login.LoginFragment -import com.meloda.fast.screens.main.MainFragment -import com.meloda.fast.screens.messages.ForwardedMessagesFragment -import com.meloda.fast.screens.messages.MessagesHistoryFragment -import com.meloda.fast.screens.settings.SettingsFragment -import com.meloda.fast.screens.updates.UpdatesFragment -import com.meloda.fast.screens.userbanned.UserBannedFragment - -@Suppress("FunctionName") -object Screens { - fun Main() = FragmentScreen { MainFragment.newInstance() } - - fun Login() = FragmentScreen { LoginFragment.newInstance() } - - fun Conversations() = FragmentScreen { ConversationsFragment() } - - fun MessagesHistory( - conversation: VkConversationDomain, - user: VkUser?, - group: VkGroup? - ) = FragmentScreen { MessagesHistoryFragment.newInstance(conversation, user, group) } - - fun ForwardedMessages( - conversation: VkConversationDomain, - messages: List, - profiles: HashMap = hashMapOf(), - groups: HashMap = hashMapOf() - ) = FragmentScreen { - ForwardedMessagesFragment.newInstance( - conversation, messages, profiles, groups - ) - } - - fun ChatInfo( - conversation: VkConversationDomain, - user: VkUser?, - group: VkGroup? - ) = FragmentScreen { ChatInfoFragment.newInstance(conversation, user, group) } - - fun Updates(updateItem: UpdateItem? = null) = - FragmentScreen { UpdatesFragment.newInstance(updateItem) } - - fun Settings() = FragmentScreen { SettingsFragment.newInstance() } - - fun UserBanned( - memberName: String, - message: String, - restoreUrl: String, - accessToken: String - ) = FragmentScreen { - UserBannedFragment.newInstance( - memberName, message, restoreUrl, accessToken - ) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/common/UpdateManager.kt b/app/src/main/kotlin/com/meloda/fast/common/UpdateManager.kt deleted file mode 100644 index 5ea0d868..00000000 --- a/app/src/main/kotlin/com/meloda/fast/common/UpdateManager.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.meloda.fast.common - -import com.meloda.fast.BuildConfig -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.ota.OtaGetLatestReleaseResponse -import com.meloda.fast.data.ota.OtaApi -import com.meloda.fast.model.UpdateActualUrl -import com.meloda.fast.model.UpdateItem -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch -import java.net.URLEncoder -import kotlin.coroutines.CoroutineContext - -interface UpdateManager { - val stateFlow: Flow - - fun checkUpdates(): Job -} - -class UpdateManagerImpl(private val repo: OtaApi) : UpdateManager { - - private val coroutineContext: CoroutineContext - get() = Dispatchers.IO - - private val coroutineScope = CoroutineScope(coroutineContext) - - private var otaBaseUrl: String? = null - - override val stateFlow = MutableStateFlow(UpdateManagerState.EMPTY) - - override fun checkUpdates() = coroutineScope.launch { - val job: suspend () -> ApiAnswer = { repo.getActualUrl() } - - when (val jobResponse = job()) { - is ApiAnswer.Success -> { - val item = jobResponse.data - otaBaseUrl = item.url - - getLatestRelease() - } - is ApiAnswer.Error -> { - otaBaseUrl = null - val throwable = jobResponse.error.throwable - - val newForm = stateFlow.value.copy( - updateItem = null, - throwable = throwable - ) - stateFlow.emit(newForm) - } - } - } - - private fun getLatestRelease() = coroutineScope.launch { - val url = "$otaBaseUrl/releases-latest" - - val job: suspend () -> ApiAnswer> = { - repo.getLatestRelease(url = url, secretCode = getOtaSecret()) - } - - when (val jobResponse = job()) { - is ApiAnswer.Success -> { - val response = jobResponse.data.response ?: return@launch - val latestRelease = response.release - - val updateItem = if (latestRelease != null && - (AppGlobal.versionName - .split("_") - .getOrNull(1) != latestRelease.versionName || - AppGlobal.versionCode < latestRelease.versionCode) - ) { - latestRelease - } else { - null - } - - val newForm = stateFlow.value.copy( - updateItem = updateItem, - throwable = null - ) - - stateFlow.emit(newForm) - } - - is ApiAnswer.Error -> { - val throwable = jobResponse.error.throwable - - val newForm = stateFlow.value.copy( - updateItem = null, - throwable = throwable - ) - stateFlow.emit(newForm) - } - } - } - - private fun getOtaSecret(): String { - return URLEncoder.encode(BuildConfig.otaSecretCode, "utf-8") - } -} - -data class UpdateManagerState( - val updateItem: UpdateItem?, - val throwable: Throwable?, -) { - companion object { - val EMPTY = UpdateManagerState( - updateItem = null, throwable = null - ) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/common/di/ApplicationModule.kt b/app/src/main/kotlin/com/meloda/fast/common/di/ApplicationModule.kt deleted file mode 100644 index 58784bac..00000000 --- a/app/src/main/kotlin/com/meloda/fast/common/di/ApplicationModule.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.meloda.fast.common.di - -import com.meloda.fast.di.apiModule -import com.meloda.fast.di.dataModule -import com.meloda.fast.di.databaseModule -import com.meloda.fast.di.navigationModule -import com.meloda.fast.di.networkModule -import com.meloda.fast.di.otaModule -import com.meloda.fast.screens.captcha.di.captchaModule -import com.meloda.fast.screens.chatinfo.di.chatInfoModule -import com.meloda.fast.screens.conversations.di.conversationsModule -import com.meloda.fast.screens.login.di.loginModule -import com.meloda.fast.screens.main.di.mainModule -import com.meloda.fast.screens.messages.di.messagesHistoryModule -import com.meloda.fast.screens.photos.di.photoViewModule -import com.meloda.fast.screens.settings.di.settingsModule -import com.meloda.fast.screens.twofa.di.twoFaModule -import com.meloda.fast.screens.updates.di.updatesModule -import org.koin.dsl.module - -val applicationModule = module { - includes( - navigationModule, - databaseModule, - dataModule, - otaModule, - networkModule, - apiModule, - loginModule, - twoFaModule, - captchaModule, - mainModule, - conversationsModule, - chatInfoModule, - settingsModule, - updatesModule, - messagesHistoryModule, - photoViewModule, - ) -} diff --git a/app/src/main/kotlin/com/meloda/fast/compose/Dialogs.kt b/app/src/main/kotlin/com/meloda/fast/compose/Dialogs.kt deleted file mode 100644 index 4f03a816..00000000 --- a/app/src/main/kotlin/com/meloda/fast/compose/Dialogs.kt +++ /dev/null @@ -1,163 +0,0 @@ -package com.meloda.fast.compose - -import androidx.compose.animation.* -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import com.meloda.fast.ext.getString -import com.meloda.fast.model.base.UiText -import com.meloda.fast.ui.AppTheme - -@Composable -fun MaterialDialog( - onDismissAction: (() -> Unit), - title: UiText? = null, - message: UiText? = null, - positiveText: UiText? = null, - positiveAction: (() -> Unit)? = null, - negativeText: UiText? = null, - negativeAction: (() -> Unit)? = null, - neutralText: UiText? = null, - neutralAction: (() -> Unit)? = null, - content: (@Composable () -> Unit)? = null -) { - var isVisible by remember { - mutableStateOf(true) - } - val onDismissRequest = { - onDismissAction.invoke() - isVisible = false - } - - AppTheme { - // TODO: 08.04.2023, Danil Nikolaev: implement animation - AlertAnimation(visible = isVisible) { - Dialog(onDismissRequest = onDismissRequest) { - val scrollState = rememberScrollState() - val canScrollBackward by remember { derivedStateOf { scrollState.value > 0 } } - val canScrollForward by remember { derivedStateOf { scrollState.value < scrollState.maxValue } } - - Surface( - modifier = Modifier.fillMaxWidth(), - color = AlertDialogDefaults.containerColor, - shape = AlertDialogDefaults.shape, - tonalElevation = AlertDialogDefaults.TonalElevation - ) { - Column( - modifier = Modifier.padding( - start = 20.dp, - top = 20.dp, - end = 20.dp, - bottom = 10.dp - ) - ) { - Row { - title?.getString()?.let { title -> - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = title, - style = MaterialTheme.typography.headlineSmall - ) - } - } - - if (canScrollBackward) { - Divider(modifier = Modifier.fillMaxWidth()) - } - - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f, fill = false) - .verticalScroll(scrollState) - ) { - Spacer(modifier = Modifier.height(8.dp)) - Row { - message?.getString()?.let { message -> - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = message, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Spacer(modifier = Modifier.height(8.dp)) - - content?.let { content -> - Spacer(modifier = Modifier.height(4.dp)) - content.invoke() - Spacer(modifier = Modifier.height(10.dp)) - } - } - - if (canScrollForward) { - Divider(modifier = Modifier.fillMaxWidth()) - } - - Row { - neutralText?.getString()?.let { text -> - TextButton( - onClick = { - onDismissRequest.invoke() - neutralAction?.invoke() - } - ) { - Text(text = text) - } - } - - Spacer(modifier = Modifier.weight(1f)) - - negativeText?.getString()?.let { text -> - TextButton( - onClick = { - onDismissRequest.invoke() - negativeAction?.invoke() - } - ) { - Text(text = text) - } - } - - Spacer(modifier = Modifier.width(2.dp)) - - positiveText?.getString()?.let { text -> - TextButton( - onClick = { - onDismissRequest.invoke() - positiveAction?.invoke() - } - ) { - Text(text = text) - } - } - } - } - } - } - } - } -} - -@OptIn(ExperimentalAnimationApi::class) -@Composable -fun AlertAnimation( - visible: Boolean, - content: @Composable AnimatedVisibilityScope.() -> Unit -) { - AnimatedVisibility( - visible = visible, - enter = fadeIn(animationSpec = tween(400)) + - scaleIn(animationSpec = tween(400)), - exit = fadeOut(animationSpec = tween(150)), - content = content - ) -} diff --git a/app/src/main/kotlin/com/meloda/fast/data/account/AccountApi.kt b/app/src/main/kotlin/com/meloda/fast/data/account/AccountApi.kt deleted file mode 100644 index bbd182e2..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/account/AccountApi.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.meloda.fast.data.account - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.account.AccountUrls -import retrofit2.http.GET -import retrofit2.http.POST -import retrofit2.http.QueryMap - -interface AccountApi { - - @GET(AccountUrls.SetOnline) - suspend fun setOnline(@QueryMap params: Map): ApiAnswer> - - @POST(AccountUrls.SetOffline) - suspend fun setOffline(@QueryMap params: Map): ApiAnswer> - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/account/AccountsDao.kt b/app/src/main/kotlin/com/meloda/fast/data/account/AccountsDao.kt deleted file mode 100644 index bbd3f79f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/account/AccountsDao.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.meloda.fast.data.account - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.meloda.fast.model.AppAccount - -@Dao -interface AccountsDao { - - @Query("SELECT * FROM accounts") - suspend fun getAll(): List - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(values: List) - - @Query("DELETE FROM accounts WHERE userId = :userId") - suspend fun deleteById(userId: Int) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/data/account/AccountsRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/account/AccountsRepository.kt deleted file mode 100644 index cf427fab..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/account/AccountsRepository.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.meloda.fast.data.account - -import com.meloda.fast.api.network.account.AccountSetOfflineRequest -import com.meloda.fast.api.network.account.AccountSetOnlineRequest - -class AccountsRepository( - private val accountApi: AccountApi, - private val accountsDao: AccountsDao -) { - - suspend fun setOnline(params: AccountSetOnlineRequest) = accountApi.setOnline(params.map) - - suspend fun setOffline(params: AccountSetOfflineRequest) = accountApi.setOffline(params.map) - - - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/audios/AudiosApi.kt b/app/src/main/kotlin/com/meloda/fast/data/audios/AudiosApi.kt deleted file mode 100644 index 1a3a33d3..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/audios/AudiosApi.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.meloda.fast.data.audios - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.model.base.attachments.BaseVkAudio -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.audio.AudiosGetUploadServerResponse -import com.meloda.fast.api.network.audio.AudiosUploadResponse -import com.meloda.fast.api.network.audio.AudiosUrls -import okhttp3.MultipartBody -import retrofit2.http.* - -interface AudiosApi { - - @POST(AudiosUrls.GetUploadServer) - suspend fun getUploadServer(): ApiAnswer> - - @Multipart - @POST - suspend fun upload( - @Url url: String, - @Part file: MultipartBody.Part - ): ApiAnswer - - @FormUrlEncoded - @POST(AudiosUrls.Save) - suspend fun save(@FieldMap map: Map): ApiAnswer> - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/audios/AudiosRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/audios/AudiosRepository.kt deleted file mode 100644 index 5a6a145e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/audios/AudiosRepository.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.meloda.fast.data.audios - -import okhttp3.MultipartBody - -class AudiosRepository( - private val audiosApi: AudiosApi -) { - - suspend fun getUploadServer() = audiosApi.getUploadServer() - - suspend fun upload(url: String, file: MultipartBody.Part) = audiosApi.upload(url, file) - - suspend fun save(server: Int, audio: String, hash: String) = audiosApi.save( - mapOf( - "server" to server.toString(), - "audio" to audio, - "hash" to hash - ) - ) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/auth/AuthApi.kt b/app/src/main/kotlin/com/meloda/fast/data/auth/AuthApi.kt deleted file mode 100644 index feb0bf98..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/auth/AuthApi.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.meloda.fast.data.auth - -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.auth.AuthDirectResponse -import com.meloda.fast.api.network.auth.AuthUrls -import com.meloda.fast.api.network.auth.SendSmsResponse -import retrofit2.http.GET -import retrofit2.http.Query -import retrofit2.http.QueryMap - -interface AuthApi { - - @GET(AuthUrls.DirectAuth) - suspend fun auth(@QueryMap param: Map): ApiAnswer - - @GET(AuthUrls.SendSms) - suspend fun sendSms(@Query("sid") validationSid: String): ApiAnswer - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/auth/AuthRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/auth/AuthRepository.kt deleted file mode 100644 index 9042d9e5..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/auth/AuthRepository.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.meloda.fast.data.auth - -import com.meloda.fast.api.network.auth.AuthDirectRequest - -class AuthRepository( - private val authApi: AuthApi -) { - - suspend fun auth(params: AuthDirectRequest) = authApi.auth(params.map) - - suspend fun sendSms(validationSid: String) = authApi.sendSms(validationSid) - - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsApi.kt b/app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsApi.kt deleted file mode 100644 index 02b939e9..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsApi.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.meloda.fast.data.conversations - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.conversations.ConversationsGetResponse -import com.meloda.fast.api.network.conversations.ConversationsUrls -import retrofit2.http.FieldMap -import retrofit2.http.FormUrlEncoded -import retrofit2.http.POST - -interface ConversationsApi { - - @FormUrlEncoded - @POST(ConversationsUrls.Get) - suspend fun get(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(ConversationsUrls.Delete) - suspend fun delete(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(ConversationsUrls.Pin) - suspend fun pin(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(ConversationsUrls.Unpin) - suspend fun unpin(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(ConversationsUrls.ReorderPinned) - suspend fun reorderPinned(@FieldMap params: Map): ApiAnswer> - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsDao.kt b/app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsDao.kt deleted file mode 100644 index 0804a222..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsDao.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.meloda.fast.data.conversations - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.meloda.fast.api.model.domain.VkConversationDomain - -@Dao -interface ConversationsDao { - - @Query("SELECT * FROM conversations") - suspend fun getAll(): List - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(values: List) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsRepository.kt deleted file mode 100644 index 9fd2049e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/conversations/ConversationsRepository.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.meloda.fast.data.conversations - -import com.meloda.fast.api.model.domain.VkConversationDomain -import com.meloda.fast.api.network.conversations.ConversationsDeleteRequest -import com.meloda.fast.api.network.conversations.ConversationsGetRequest -import com.meloda.fast.api.network.conversations.ConversationsPinRequest -import com.meloda.fast.api.network.conversations.ConversationsUnpinRequest - -class ConversationsRepository( - private val conversationsApi: ConversationsApi, - private val conversationsDao: ConversationsDao -) { - - suspend fun get(params: ConversationsGetRequest) = conversationsApi.get(params.map) - - suspend fun delete(params: ConversationsDeleteRequest) = conversationsApi.delete(params.map) - - suspend fun pin(params: ConversationsPinRequest) = conversationsApi.pin(params.map) - - suspend fun unpin(params: ConversationsUnpinRequest) = conversationsApi.unpin(params.map) - - suspend fun store(conversations: List) = conversationsDao.insert(conversations) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/data/files/FilesApi.kt b/app/src/main/kotlin/com/meloda/fast/data/files/FilesApi.kt deleted file mode 100644 index 73cdad7e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/files/FilesApi.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.meloda.fast.data.files - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.files.FilesGetMessagesUploadServerResponse -import com.meloda.fast.api.network.files.FilesSaveFileResponse -import com.meloda.fast.api.network.files.FilesUploadFileResponse -import com.meloda.fast.api.network.files.FilesUrls -import okhttp3.MultipartBody -import retrofit2.http.* - -interface FilesApi { - - @FormUrlEncoded - @POST(FilesUrls.GetMessagesUploadServer) - suspend fun getUploadServer( - @FieldMap map: Map - ): ApiAnswer> - - @Multipart - @POST - suspend fun upload( - @Url url: String, - @Part file: MultipartBody.Part - ): ApiAnswer - - @FormUrlEncoded - @POST(FilesUrls.Save) - suspend fun save( - @FieldMap map: Map - ): ApiAnswer> - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/files/FilesRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/files/FilesRepository.kt deleted file mode 100644 index ec894800..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/files/FilesRepository.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.meloda.fast.data.files - -import com.google.gson.annotations.SerializedName -import okhttp3.MultipartBody - -class FilesRepository( - private val filesApi: FilesApi -) { - - enum class FileType(val value: String) { - @SerializedName("doc") - File("doc"), - - @SerializedName("audio_message") - VoiceMessage("audio_message") - } - - suspend fun getMessagesUploadServer(peerId: Int, type: FileType) = - filesApi.getUploadServer( - mapOf( - "peer_id" to peerId.toString(), - "type" to type.value - ) - ) - - suspend fun uploadFile(url: String, file: MultipartBody.Part) = filesApi.upload(url, file) - - suspend fun saveMessageFile(file: String) = filesApi.save(mapOf("file" to file)) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/groups/GroupsDao.kt b/app/src/main/kotlin/com/meloda/fast/data/groups/GroupsDao.kt deleted file mode 100644 index 87d7ae6e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/groups/GroupsDao.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.meloda.fast.data.groups - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.meloda.fast.api.model.VkGroup - -@Dao -interface GroupsDao { - - @Query("SELECT * FROM groups") - suspend fun getAll(): List - - @Query("SELECT * FROM groups WHERE id = :id") - suspend fun getById(id: Int): VkGroup? - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(values: List) - - suspend fun insert(values: Array) = insert(values.toList()) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/groups/GroupsRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/groups/GroupsRepository.kt deleted file mode 100644 index 50ba5c0d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/groups/GroupsRepository.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.meloda.fast.data.groups - -class GroupsRepository( - private val groupsDao: GroupsDao -) { -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/longpoll/LongPollApi.kt b/app/src/main/kotlin/com/meloda/fast/data/longpoll/LongPollApi.kt deleted file mode 100644 index a2ab6688..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/longpoll/LongPollApi.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.meloda.fast.data.longpoll - -import com.google.gson.JsonObject -import com.meloda.fast.api.network.ApiAnswer -import retrofit2.http.GET -import retrofit2.http.QueryMap -import retrofit2.http.Url - -interface LongPollApi { - - @GET - suspend fun getResponse( - @Url serverUrl: String, - @QueryMap params: Map - ): ApiAnswer - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/messages/MessagesApi.kt b/app/src/main/kotlin/com/meloda/fast/data/messages/MessagesApi.kt deleted file mode 100644 index c12d1fd0..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/messages/MessagesApi.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.meloda.fast.data.messages - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.model.base.BaseVkChat -import com.meloda.fast.api.model.base.BaseVkLongPoll -import com.meloda.fast.api.model.base.BaseVkMessage -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.messages.MessagesGetByIdResponse -import com.meloda.fast.api.network.messages.MessagesGetConversationMembersResponse -import com.meloda.fast.api.network.messages.MessagesGetHistoryResponse -import com.meloda.fast.api.network.messages.MessagesUrls -import retrofit2.http.FieldMap -import retrofit2.http.FormUrlEncoded -import retrofit2.http.POST - -interface MessagesApi { - - @FormUrlEncoded - @POST(MessagesUrls.GetHistory) - suspend fun getHistory(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.Send) - suspend fun send(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.MarkAsImportant) - suspend fun markAsImportant(@FieldMap params: Map): ApiAnswer>> - - @FormUrlEncoded - @POST(MessagesUrls.GetLongPollServer) - suspend fun getLongPollServer(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.Pin) - suspend fun pin(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.Unpin) - suspend fun unpin(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.Delete) - suspend fun delete(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.Edit) - suspend fun edit(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.GetById) - suspend fun getById(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.MarkAsRead) - suspend fun markAsRead(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.GetChat) - suspend fun getChat(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.GetConversationMembers) - suspend fun getConversationMembers(@FieldMap params: Map): ApiAnswer> - - @FormUrlEncoded - @POST(MessagesUrls.RemoveChatUser) - suspend fun removeChatUser(@FieldMap params: Map): ApiAnswer> - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/messages/MessagesDao.kt b/app/src/main/kotlin/com/meloda/fast/data/messages/MessagesDao.kt deleted file mode 100644 index 0953ae5e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/messages/MessagesDao.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.data.messages - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.meloda.fast.api.model.VkMessage - -@Dao -interface MessagesDao { - - @Query("SELECT * FROM messages") - suspend fun getAll(): List - - @Query("SELECT * FROM messages WHERE id = :id") - suspend fun getById(id: Int): VkMessage? - - @Query("SELECT * FROM messages WHERE peerId = :peerId") - suspend fun getByPeerId(peerId: Int): List - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(values: List) - - suspend fun insert(values: Array) = insert(values.toList()) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/messages/MessagesRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/messages/MessagesRepository.kt deleted file mode 100644 index f1c70b60..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/messages/MessagesRepository.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.meloda.fast.data.messages - -import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.network.longpoll.LongPollGetUpdatesRequest -import com.meloda.fast.api.network.messages.MessagesDeleteRequest -import com.meloda.fast.api.network.messages.MessagesEditRequest -import com.meloda.fast.api.network.messages.MessagesGetByIdRequest -import com.meloda.fast.api.network.messages.MessagesGetChatRequest -import com.meloda.fast.api.network.messages.MessagesGetConversationMembersRequest -import com.meloda.fast.api.network.messages.MessagesGetHistoryRequest -import com.meloda.fast.api.network.messages.MessagesGetLongPollServerRequest -import com.meloda.fast.api.network.messages.MessagesMarkAsImportantRequest -import com.meloda.fast.api.network.messages.MessagesPinMessageRequest -import com.meloda.fast.api.network.messages.MessagesRemoveChatUserRequest -import com.meloda.fast.api.network.messages.MessagesSendRequest -import com.meloda.fast.api.network.messages.MessagesUnPinMessageRequest -import com.meloda.fast.data.longpoll.LongPollApi - -class MessagesRepository( - private val messagesApi: MessagesApi, - private val messagesDao: MessagesDao, - private val longPollApi: LongPollApi, -) { - - suspend fun store(message: VkMessage) = store(listOf(message)) - - suspend fun store(messages: List) = messagesDao.insert(messages) - - suspend fun getCached(peerId: Int) = messagesDao.getByPeerId(peerId) - - suspend fun getHistory(params: MessagesGetHistoryRequest) = - messagesApi.getHistory(params.map) - - suspend fun send(params: MessagesSendRequest) = - messagesApi.send(params.map) - - suspend fun markAsImportant(params: MessagesMarkAsImportantRequest) = - messagesApi.markAsImportant(params.map) - - suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) = - messagesApi.getLongPollServer(params.map) - - suspend fun pin(params: MessagesPinMessageRequest) = - messagesApi.pin(params.map) - - suspend fun unpin(params: MessagesUnPinMessageRequest) = - messagesApi.unpin(params.map) - - suspend fun delete(params: MessagesDeleteRequest) = - messagesApi.delete(params.map) - - suspend fun edit(params: MessagesEditRequest) = - messagesApi.edit(params.map) - - suspend fun getLongPollUpdates( - serverUrl: String, - params: LongPollGetUpdatesRequest, - ) = longPollApi.getResponse(serverUrl, params.map) - - suspend fun getById(params: MessagesGetByIdRequest) = - messagesApi.getById(params.map) - - suspend fun markAsRead( - peerId: Int, - messagesIds: List? = null, - startMessageId: Int? = null, - ) = messagesApi.markAsRead( - mutableMapOf("peer_id" to peerId.toString()).apply { - messagesIds?.let { - this["message_ids"] = messagesIds.joinToString { it.toString() } - } - startMessageId?.let { - this["start_message_id"] = it.toString() - } - } - ) - - suspend fun getChat( - chatId: Int, - fields: String? = null, - ) = messagesApi.getChat(MessagesGetChatRequest(chatId, fields).map) - - suspend fun getConversationMembers( - peerId: Int, - offset: Int? = null, - count: Int? = null, - extended: Boolean? = null, - fields: String? = null, - ) = messagesApi.getConversationMembers( - MessagesGetConversationMembersRequest( - peerId, - offset, - count, - extended, - fields - ).map - ) - - suspend fun removeChatUser( - chatId: Int, - memberId: Int, - ) = messagesApi.removeChatUser(MessagesRemoveChatUserRequest(chatId, memberId).map) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/data/ota/OtaApi.kt b/app/src/main/kotlin/com/meloda/fast/data/ota/OtaApi.kt deleted file mode 100644 index 68ea9737..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/ota/OtaApi.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.data.ota - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.ota.OtaGetLatestReleaseResponse -import com.meloda.fast.api.network.ota.OtaUrls -import com.meloda.fast.model.UpdateActualUrl -import retrofit2.http.GET -import retrofit2.http.Header -import retrofit2.http.Query -import retrofit2.http.Url - -interface OtaApi { - - @GET(OtaUrls.GetActualUrl) - suspend fun getActualUrl(): ApiAnswer - - @GET - suspend fun getLatestRelease( - @Url url: String, - @Query("productId") productId: Int = 28, - @Query("branchId") branchId: Int = 10, - @Header("Secret-Code") secretCode: String - ): ApiAnswer> - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/photos/PhotosApi.kt b/app/src/main/kotlin/com/meloda/fast/data/photos/PhotosApi.kt deleted file mode 100644 index 863388e0..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/photos/PhotosApi.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.meloda.fast.data.photos - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.model.base.attachments.BaseVkPhoto -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.photos.PhotoUrls -import com.meloda.fast.api.network.photos.PhotosGetMessagesUploadServerResponse -import com.meloda.fast.api.network.photos.PhotosUploadPhotoResponse -import okhttp3.MultipartBody -import retrofit2.http.* - -interface PhotosApi { - - @FormUrlEncoded - @POST(PhotoUrls.GetMessagesUploadServer) - suspend fun getUploadServer( - @FieldMap map: Map - ): ApiAnswer> - - @Multipart - @POST - suspend fun upload( - @Url url: String, - @Part photo: MultipartBody.Part - ): ApiAnswer - - @FormUrlEncoded - @POST(PhotoUrls.SaveMessagePhoto) - suspend fun save( - @FieldMap map: Map - ): ApiAnswer>> - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/photos/PhotosRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/photos/PhotosRepository.kt deleted file mode 100644 index e143c7a6..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/photos/PhotosRepository.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.meloda.fast.data.photos - -import com.meloda.fast.api.network.photos.PhotosSaveMessagePhotoRequest -import okhttp3.MultipartBody - -class PhotosRepository( - private val photosApi: PhotosApi -) { - - suspend fun getMessagesUploadServer(peerId: Int) = - photosApi.getUploadServer(mapOf("peer_id" to peerId.toString())) - - suspend fun uploadPhoto(url: String, photo: MultipartBody.Part) = photosApi.upload(url, photo) - - suspend fun saveMessagePhoto(body: PhotosSaveMessagePhotoRequest) = - photosApi.save(body.map) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/users/UsersApi.kt b/app/src/main/kotlin/com/meloda/fast/data/users/UsersApi.kt deleted file mode 100644 index 7574536a..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/users/UsersApi.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.meloda.fast.data.users - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.model.base.BaseVkUser -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.users.UsersUrls -import retrofit2.http.FieldMap -import retrofit2.http.FormUrlEncoded -import retrofit2.http.POST - -interface UsersApi { - - @FormUrlEncoded - @POST(UsersUrls.GetById) - suspend fun getById( - @FieldMap params: Map? - ): ApiAnswer>> - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/users/UsersDao.kt b/app/src/main/kotlin/com/meloda/fast/data/users/UsersDao.kt deleted file mode 100644 index 3d74af1d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/users/UsersDao.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.meloda.fast.data.users - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.meloda.fast.api.model.VkUser - -@Dao -interface UsersDao { - - @Query("SELECT * FROM users") - suspend fun getAll(): List - - @Query("SELECT * FROM users WHERE id = :id") - suspend fun getById(id: Int): VkUser? - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(values: List) - - suspend fun insert(values: Array) = insert(values.toList()) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/users/UsersRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/users/UsersRepository.kt deleted file mode 100644 index f41f0b17..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/users/UsersRepository.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.meloda.fast.data.users - -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.network.users.UsersGetRequest - -class UsersRepository( - private val usersApi: UsersApi, - private val usersDao: UsersDao -) { - - suspend fun getById(params: UsersGetRequest) = usersApi.getById(params.map) - - suspend fun storeUsers(users: List) { - usersDao.insert(users) - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/videos/VideosApi.kt b/app/src/main/kotlin/com/meloda/fast/data/videos/VideosApi.kt deleted file mode 100644 index 1b244480..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/videos/VideosApi.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.data.videos - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.videos.VideosSaveResponse -import com.meloda.fast.api.network.videos.VideosUploadResponse -import com.meloda.fast.api.network.videos.VideosUrls -import okhttp3.MultipartBody -import retrofit2.http.Multipart -import retrofit2.http.POST -import retrofit2.http.Part -import retrofit2.http.Url - -interface VideosApi { - - @POST(VideosUrls.Save) - suspend fun save(): ApiAnswer> - - @Multipart - @POST - suspend fun upload( - @Url url: String, - @Part file: MultipartBody.Part - ): ApiAnswer - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/data/videos/VideosRepository.kt b/app/src/main/kotlin/com/meloda/fast/data/videos/VideosRepository.kt deleted file mode 100644 index 0b9f2866..00000000 --- a/app/src/main/kotlin/com/meloda/fast/data/videos/VideosRepository.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.meloda.fast.data.videos - -import okhttp3.MultipartBody - -class VideosRepository( - private val videosApi: VideosApi -) { - - suspend fun save() = videosApi.save() - - suspend fun upload(url: String, file: MultipartBody.Part) = videosApi.upload(url, file) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/database/AccountsDatabase.kt b/app/src/main/kotlin/com/meloda/fast/database/AccountsDatabase.kt deleted file mode 100644 index a5ec4ef4..00000000 --- a/app/src/main/kotlin/com/meloda/fast/database/AccountsDatabase.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.meloda.fast.database - -import androidx.room.Database -import androidx.room.RoomDatabase -import com.meloda.fast.data.account.AccountsDao -import com.meloda.fast.model.AppAccount - -@Database( - entities = [AppAccount::class], - version = 1, - exportSchema = false -) -abstract class AccountsDatabase : RoomDatabase() { - abstract val accountsDao: AccountsDao -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/database/CacheDatabase.kt b/app/src/main/kotlin/com/meloda/fast/database/CacheDatabase.kt deleted file mode 100644 index fa8df9ea..00000000 --- a/app/src/main/kotlin/com/meloda/fast/database/CacheDatabase.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.meloda.fast.database - -import androidx.room.Database -import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.model.domain.VkConversationDomain -import com.meloda.fast.data.conversations.ConversationsDao -import com.meloda.fast.data.groups.GroupsDao -import com.meloda.fast.data.messages.MessagesDao -import com.meloda.fast.data.users.UsersDao - -@Database( - entities = [ - VkConversationDomain::class, - VkMessage::class, - VkUser::class, - VkGroup::class - ], - version = 42, - exportSchema = false -) -@TypeConverters(Converters::class) -abstract class CacheDatabase : RoomDatabase() { - - abstract val conversationsDao: ConversationsDao - abstract val messagesDao: MessagesDao - abstract val usersDao: UsersDao - abstract val groupsDao: GroupsDao - -} diff --git a/app/src/main/kotlin/com/meloda/fast/database/Converters.kt b/app/src/main/kotlin/com/meloda/fast/database/Converters.kt deleted file mode 100644 index 5b852294..00000000 --- a/app/src/main/kotlin/com/meloda/fast/database/Converters.kt +++ /dev/null @@ -1,160 +0,0 @@ -package com.meloda.fast.database - -import androidx.room.TypeConverter -import com.google.gson.Gson -import com.meloda.fast.api.base.AttachmentClassNameIsEmptyException -import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.model.attachments.VkAttachment -import com.meloda.fast.api.model.base.BaseVkMessage -import org.json.JSONObject - -@Suppress("UnnecessaryVariable") -class Converters { - - private companion object { - private const val CACHE_SEPARATOR = "fastkruta228355" - } - - @TypeConverter - fun fromGeoToString(geo: BaseVkMessage.Geo?): String? { - if (geo == null) return null - - return try { - val string = Gson().toJson(geo) - - return string - } catch (e: Exception) { - e.printStackTrace() - null - } - } - - @TypeConverter - fun fromStringToGeo(string: String?): BaseVkMessage.Geo? { - if (string == null) return null - - return try { - val geo = Gson().fromJson(string, BaseVkMessage.Geo::class.java) - - return geo - } catch (e: Exception) { - e.printStackTrace() - null - } - } - - @TypeConverter - fun fromListVkMessageToString(messages: List?): String? { - if (messages == null) return null - - val string = messages - .mapNotNull(::fromVkMessageToString) - .joinToString(separator = CACHE_SEPARATOR) - - return string - } - - @TypeConverter - fun fromStringToListVkMessage(string: String?): List? { - if (string == null) return null - - if (string.contains(CACHE_SEPARATOR)) { - val messages = string - .split(CACHE_SEPARATOR) - .mapNotNull(::fromStringToVkMessage) - return messages - } - - - val message = fromStringToVkMessage(string) - return message?.let { listOf(it) } - } - - @TypeConverter - fun fromVkMessageToString(message: VkMessage?): String? { - if (message == null) return null - - return try { - val string = Gson().toJson(message) - - return string - } catch (e: Exception) { - e.printStackTrace() - null - } - } - - @TypeConverter - fun fromStringToVkMessage(string: String?): VkMessage? { - if (string == null) return null - - return try { - val message = Gson().fromJson(string, VkMessage::class.java) - - return message - } catch (e: Exception) { - e.printStackTrace() - null - } - } - - @TypeConverter - fun fromListVkAttachmentToString(attachments: List?): String? { - if (attachments == null) return null - - val string = attachments - .mapNotNull(::fromVkAttachmentToString) - .joinToString(separator = CACHE_SEPARATOR) - return string - } - - @TypeConverter - fun fromStringToListVkAttachment(string: String?): List? { - if (string == null) return null - - if (string.contains(CACHE_SEPARATOR)) { - val attachments = string - .split(CACHE_SEPARATOR) - .mapNotNull(::fromStringToVkAttachment) - return attachments - } - - val attachment = fromStringToVkAttachment(string) - - return attachment?.let { listOf(it) } - } - - @TypeConverter - fun fromVkAttachmentToString(attachment: VkAttachment?): String? { - if (attachment == null) return null - - try { - attachment.javaClass.getDeclaredField("className") - } catch (e: NoSuchFieldException) { - throw AttachmentClassNameIsEmptyException(attachment) - } - return try { - val string = Gson().toJson(attachment) - string - } catch (e: Exception) { - e.printStackTrace() - null - } - } - - @TypeConverter - fun fromStringToVkAttachment(string: String?): VkAttachment? { - if (string.isNullOrBlank()) return null - - return try { - val className = JSONObject(string).optString("className") - - val attachment = Gson().fromJson(string, Class.forName(className)) as? VkAttachment? - - return attachment - } catch (e: Exception) { - e.printStackTrace() - null - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/di/ApiModule.kt b/app/src/main/kotlin/com/meloda/fast/di/ApiModule.kt deleted file mode 100644 index 275c38b5..00000000 --- a/app/src/main/kotlin/com/meloda/fast/di/ApiModule.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.meloda.fast.di - -import com.meloda.fast.api.longpoll.LongPollUpdatesParser -import com.meloda.fast.data.account.AccountApi -import com.meloda.fast.data.audios.AudiosApi -import com.meloda.fast.data.auth.AuthApi -import com.meloda.fast.data.conversations.ConversationsApi -import com.meloda.fast.data.files.FilesApi -import com.meloda.fast.data.longpoll.LongPollApi -import com.meloda.fast.data.messages.MessagesApi -import com.meloda.fast.data.ota.OtaApi -import com.meloda.fast.data.photos.PhotosApi -import com.meloda.fast.data.users.UsersApi -import com.meloda.fast.data.videos.VideosApi -import org.koin.core.module.dsl.singleOf -import org.koin.core.scope.Scope -import org.koin.dsl.module - -val apiModule = module { - single { api(AuthApi::class.java) } - single { api(ConversationsApi::class.java) } - single { api(UsersApi::class.java) } - single { api(MessagesApi::class.java) } - single { api(LongPollApi::class.java) } - single { api(AccountApi::class.java) } - single { api(OtaApi::class.java) } - single { api(PhotosApi::class.java) } - single { api(VideosApi::class.java) } - single { api(AudiosApi::class.java) } - single { api(FilesApi::class.java) } - - singleOf(::LongPollUpdatesParser) -} - -internal fun Scope.api(className: Class): T = retrofit().create(className) diff --git a/app/src/main/kotlin/com/meloda/fast/di/DataModule.kt b/app/src/main/kotlin/com/meloda/fast/di/DataModule.kt deleted file mode 100644 index 9397fd8a..00000000 --- a/app/src/main/kotlin/com/meloda/fast/di/DataModule.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.di - -import com.meloda.fast.data.account.AccountsRepository -import com.meloda.fast.data.audios.AudiosRepository -import com.meloda.fast.data.auth.AuthRepository -import com.meloda.fast.data.conversations.ConversationsRepository -import com.meloda.fast.data.files.FilesRepository -import com.meloda.fast.data.messages.MessagesRepository -import com.meloda.fast.data.photos.PhotosRepository -import com.meloda.fast.data.users.UsersRepository -import com.meloda.fast.data.videos.VideosRepository -import org.koin.core.module.dsl.singleOf -import org.koin.dsl.module - -// TODO: 17.04.2023, Danil Nikolaev: use specific repositories in local DI modules -val dataModule = module { - singleOf(::ConversationsRepository) - singleOf(::MessagesRepository) - singleOf(::UsersRepository) - singleOf(::AuthRepository) - singleOf(::AccountsRepository) - singleOf(::PhotosRepository) - singleOf(::VideosRepository) - singleOf(::AudiosRepository) - singleOf(::FilesRepository) -} diff --git a/app/src/main/kotlin/com/meloda/fast/di/DatabaseModule.kt b/app/src/main/kotlin/com/meloda/fast/di/DatabaseModule.kt deleted file mode 100644 index bd5615ce..00000000 --- a/app/src/main/kotlin/com/meloda/fast/di/DatabaseModule.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.meloda.fast.di - -import androidx.room.Room -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.database.AccountsDatabase -import com.meloda.fast.database.CacheDatabase -import org.koin.core.scope.Scope -import org.koin.dsl.module - -val databaseModule = module { - single { - Room.databaseBuilder(AppGlobal.Instance, CacheDatabase::class.java, "cache") - .fallbackToDestructiveMigration() - .build() - } - single { - Room.databaseBuilder(AppGlobal.Instance, AccountsDatabase::class.java, "accounts") - .build() - } - single { cache().conversationsDao } - single { cache().messagesDao } - single { cache().usersDao } - single { cache().groupsDao } - single { accounts().accountsDao } -} - -private fun Scope.cache(): CacheDatabase = get() -private fun Scope.accounts(): AccountsDatabase = get() diff --git a/app/src/main/kotlin/com/meloda/fast/di/NavigationModule.kt b/app/src/main/kotlin/com/meloda/fast/di/NavigationModule.kt deleted file mode 100644 index 3b130ede..00000000 --- a/app/src/main/kotlin/com/meloda/fast/di/NavigationModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.meloda.fast.di - -import com.github.terrakok.cicerone.Cicerone -import com.github.terrakok.cicerone.Router -import com.meloda.fast.screens.captcha.screen.CaptchaScreen -import com.meloda.fast.screens.twofa.screen.TwoFaScreen -import org.koin.core.module.dsl.singleOf -import org.koin.core.scope.Scope -import org.koin.dsl.module - -val navigationModule = module { - single { Cicerone.create() } - single { cicerone().router } - single { cicerone().getNavigatorHolder() } - - singleOf(::CaptchaScreen) - singleOf(::TwoFaScreen) -} - -private fun Scope.cicerone(): Cicerone = get() diff --git a/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt b/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt deleted file mode 100644 index b7a546ab..00000000 --- a/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.meloda.fast.di - -import com.chuckerteam.chucker.api.ChuckerCollector -import com.chuckerteam.chucker.api.ChuckerInterceptor -import com.google.gson.GsonBuilder -import com.meloda.fast.api.network.AuthInterceptor -import com.meloda.fast.api.network.ResultCallFactory -import com.meloda.fast.api.network.VkUrls -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import org.koin.core.module.dsl.singleOf -import org.koin.core.scope.Scope -import org.koin.dsl.module -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory -import java.util.concurrent.TimeUnit - -val networkModule = module { - single { ChuckerCollector(get()) } - single { ChuckerInterceptor.Builder(get()).collector(get()).build() } - singleOf(::AuthInterceptor) - single { GsonBuilder().setLenient().create() } - single { - OkHttpClient.Builder() - .connectTimeout(20, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .addInterceptor(authInterceptor()) - .addInterceptor( - chuckerInterceptor().apply { - redactHeader("Secret-Code") - } - ) - .followRedirects(true) - .followSslRedirects(true) - .addInterceptor( - HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY - } - ).build() - } - single { - Retrofit.Builder() - .baseUrl("${VkUrls.API}/") - .addConverterFactory(GsonConverterFactory.create(get())) - .addCallAdapterFactory(ResultCallFactory(get())) - .client(get()) - .build() - } -} - -internal fun Scope.retrofit(): Retrofit = get() -private fun Scope.authInterceptor(): AuthInterceptor = get() -private fun Scope.chuckerInterceptor(): ChuckerInterceptor = get() diff --git a/app/src/main/kotlin/com/meloda/fast/di/OtaModule.kt b/app/src/main/kotlin/com/meloda/fast/di/OtaModule.kt deleted file mode 100644 index 413a16b3..00000000 --- a/app/src/main/kotlin/com/meloda/fast/di/OtaModule.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.meloda.fast.di - -import com.meloda.fast.common.UpdateManager -import com.meloda.fast.common.UpdateManagerImpl -import com.meloda.fast.data.ota.OtaApi -import org.koin.core.module.dsl.bind -import org.koin.core.module.dsl.singleOf -import org.koin.dsl.module - -val otaModule = module { - single { api(OtaApi::class.java) } - singleOf(::UpdateManagerImpl) { bind() } -} diff --git a/app/src/main/kotlin/com/meloda/fast/ext/ActivityExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/ActivityExt.kt deleted file mode 100644 index f75afa5a..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/ActivityExt.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.meloda.fast.ext - -import android.app.Activity -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.WindowCompat -import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.flow.Flow - -fun Activity.edgeToEdge() { - WindowCompat.setDecorFitsSystemWindows(window, false) -} - -context(AppCompatActivity) -fun Flow.listenValue(action: suspend (T) -> Unit) = listenValue(lifecycleScope, action) diff --git a/app/src/main/kotlin/com/meloda/fast/ext/AndroidVersionsExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/AndroidVersionsExt.kt deleted file mode 100644 index c65f885c..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/AndroidVersionsExt.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.meloda.fast.ext - -import android.os.Build - -fun isSdkAtLeast(sdkInt: Int, action: (() -> Unit)? = null): Boolean { - return if (Build.VERSION.SDK_INT >= sdkInt) { - action?.invoke() - true - } else { - false - } -} - -fun sdkAndUp(sdkInt: Int, action: () -> Unit): Boolean? { - return if (Build.VERSION.SDK_INT >= sdkInt) { - action.invoke() - true - } else null -} - -fun isSdkAtLeastOr( - sdkInt: Int, - action: (() -> Unit)? = null, - orAction: (() -> Unit)? = null -): Boolean { - return if (Build.VERSION.SDK_INT >= sdkInt) { - action?.invoke() - true - } else { - orAction?.invoke() - false - } -} - -fun sdk26AndUp(action: () -> Unit): Boolean? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - action.invoke() - true - } else null -} - -fun sdk30AndUp(action: () -> Unit): Boolean? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - action.invoke() - true - } else null -} - -fun sdk33AndUp(action: () -> Unit): Boolean? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - action.invoke() - true - } else null -} diff --git a/app/src/main/kotlin/com/meloda/fast/ext/BooleanExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/BooleanExt.kt deleted file mode 100644 index a1007d57..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/BooleanExt.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.meloda.fast.ext - -val Boolean?.isTrue: Boolean get() = this == true - -val Boolean?.isFalse: Boolean get() = this == false diff --git a/app/src/main/kotlin/com/meloda/fast/ext/BundleExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/BundleExt.kt deleted file mode 100644 index bc31d952..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/BundleExt.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.meloda.fast.ext - -import android.os.Build -import android.os.Bundle -import android.os.Parcelable -import java.io.Serializable - -@Suppress("UNCHECKED_CAST", "DEPRECATION") -fun Bundle.getParcelableArrayListCompat( - key: String?, - clazz: Class -): java.util.ArrayList? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getParcelableArrayList(key, clazz) - } else { - getParcelableArrayList(key) as ArrayList - } -} - -@Suppress("DEPRECATION") -fun Bundle.getParcelableCompat(key: String?, clazz: Class): T? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getParcelable(key, clazz) - } else { - getParcelable(key) - } -} - -@Suppress("DEPRECATION", "UNCHECKED_CAST") -fun Bundle.getSerializableCompat(key: String?, clazz: Class): T? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getSerializable(key, clazz) - } else { - getSerializable(key) as? T - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/ext/ComposeExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/ComposeExt.kt deleted file mode 100644 index 8ab3fa82..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/ComposeExt.kt +++ /dev/null @@ -1,146 +0,0 @@ -package com.meloda.fast.ext - -import android.content.res.Configuration -import android.media.AudioManager -import android.view.KeyEvent -import androidx.appcompat.app.AppCompatDelegate -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Indication -import androidx.compose.foundation.LocalIndication -import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.input.key.onKeyEvent -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.semantics.Role -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.model.base.UiText -import com.meloda.fast.model.base.parseString -import com.meloda.fast.screens.settings.SettingsFragment -import com.meloda.fast.util.AndroidUtils - -@ExperimentalFoundationApi -fun Modifier.clickableSound( - enabled: Boolean = true, - onClickLabel: String? = null, - role: Role? = null, - onClick: (() -> Unit)? = null -): Modifier = this.clickable( - enabled = enabled, - onClickLabel = onClickLabel, - role = role, - onClick = { - AppGlobal.audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK) - onClick?.invoke() - } -) - -@ExperimentalFoundationApi -fun Modifier.combinedClickableSound( - enabled: Boolean = true, - onClickLabel: String? = null, - role: Role? = null, - onLongClickLabel: String? = null, - onLongClick: (() -> Unit)? = null, - onDoubleClick: (() -> Unit)? = null, - onClick: (() -> Unit)? = null -): Modifier = composed { - this.combinedClickableSound( - interactionSource = remember { MutableInteractionSource() }, - indication = LocalIndication.current, - enabled = enabled, - onClickLabel = onClickLabel, - role = role, - onLongClickLabel = onLongClickLabel, - onLongClick = onLongClick, - onDoubleClick = onDoubleClick, - onClick = { - AppGlobal.audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK) - onClick?.invoke() - } - ) -} - -@ExperimentalFoundationApi -fun Modifier.combinedClickableSound( - interactionSource: MutableInteractionSource, - indication: Indication?, - enabled: Boolean = true, - onClickLabel: String? = null, - role: Role? = null, - onLongClickLabel: String? = null, - onLongClick: (() -> Unit)? = null, - onDoubleClick: (() -> Unit)? = null, - onClick: (() -> Unit)? = null -): Modifier = this.combinedClickable( - interactionSource = interactionSource, - indication = indication, - enabled = enabled, - onClickLabel = onClickLabel, - role = role, - onLongClickLabel = onLongClickLabel, - onLongClick = onLongClick, - onDoubleClick = onDoubleClick, - onClick = { - AppGlobal.audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK) - onClick?.invoke() - } -) - -fun Modifier.handleTabKey( - action: () -> Boolean -): Modifier = this.onKeyEvent { event -> - if (event.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_TAB) { - action.invoke() - } else false -} - -fun Modifier.handleEnterKey( - action: () -> Boolean -): Modifier = this.onKeyEvent { event -> - if (event.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_ENTER) { - action.invoke() - } else false -} - -@Composable -fun UiText?.getString(): String? { - return this.parseString(LocalContext.current) -} - -@Composable -fun isUsingDarkTheme(): Boolean { - if (LocalView.current.isInEditMode) { - return false - } - - val nightThemeMode = AppGlobal.preferences.getInt( - SettingsFragment.KEY_APPEARANCE_DARK_THEME, - SettingsFragment.DEFAULT_VALUE_APPEARANCE_DARK_THEME - ) - val appForceDarkMode = nightThemeMode == AppCompatDelegate.MODE_NIGHT_YES - val appBatterySaver = nightThemeMode == AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY - - val systemUiNightMode = AppGlobal.resources.configuration.uiMode - - val isSystemBatterySaver = AndroidUtils.isBatterySaverOn() - val isSystemUsingDarkTheme = - systemUiNightMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES - - return appForceDarkMode || (appBatterySaver && isSystemBatterySaver) || (!appBatterySaver && isSystemUsingDarkTheme && nightThemeMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) -} - -@Composable -fun isUsingDynamicColors(): Boolean = - if (LocalView.current.isInEditMode) true - else { - AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_USE_DYNAMIC_COLORS, - SettingsFragment.DEFAULT_VALUE_USE_DYNAMIC_COLORS - ) - } diff --git a/app/src/main/kotlin/com/meloda/fast/ext/ContextExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/ContextExt.kt deleted file mode 100644 index a7fd3e66..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/ContextExt.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.meloda.fast.ext - -import android.content.Context -import android.view.View -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.meloda.fast.model.base.UiText -import com.meloda.fast.model.base.parseString - -fun Context.showDialog( - title: UiText? = null, - message: UiText? = null, - isCancelable: Boolean = true, - positiveText: UiText? = null, - positiveAction: (() -> Unit)? = null, - negativeText: UiText? = null, - negativeAction: (() -> Unit)? = null, - neutralText: UiText? = null, - neutralAction: (() -> Unit)? = null, - onDismissAction: (() -> Unit)? = null, - view: View? = null, - items: List? = null, - itemsChoiceType: ItemsChoiceType = ItemsChoiceType.None, - itemsClickAction: ((index: Int, value: String) -> Unit)? = null, - itemsMultiChoiceClickAction: ((index: Int, value: String, isChecked: Boolean) -> Unit)? = null, - checkedItems: List? = null -): AlertDialog { - val builder = MaterialAlertDialogBuilder(this) - .setCancelable(isCancelable) - .setOnDismissListener { onDismissAction?.invoke() } - - title?.asString()?.let(builder::setTitle) - message?.asString()?.let(builder::setMessage) - - view?.let(builder::setView) - - positiveText?.let { text -> - builder.setPositiveButton(text.asString()) { _, _ -> positiveAction?.invoke() } - } - negativeText?.let { text -> - builder.setNegativeButton(text.asString()) { _, _ -> negativeAction?.invoke() } - } - neutralText?.let { text -> - builder.setNeutralButton(text.asString()) { _, _ -> neutralAction?.invoke() } - } - - items?.mapNotNull { it.asString() }?.let { stringItems -> - when (itemsChoiceType) { - ItemsChoiceType.None -> { - builder.setItems( - stringItems.toTypedArray() - ) { dialog, which -> - dialog.dismiss() - itemsClickAction?.invoke(which, stringItems[which]) - } - } - - ItemsChoiceType.SingleChoice -> { - builder.setSingleChoiceItems( - stringItems.toTypedArray(), - checkedItems?.first() ?: -1 - ) { _, which -> - itemsClickAction?.invoke(which, stringItems[which]) - } - } - - ItemsChoiceType.MultiChoice -> { - builder.setMultiChoiceItems( - stringItems.toTypedArray(), - BooleanArray(stringItems.size) { index -> checkedItems?.contains(index).isTrue } - ) { _, which, isChecked -> - itemsMultiChoiceClickAction?.invoke(which, stringItems[which], isChecked) - } - } - } - } - - return builder.show() -} - -sealed class ItemsChoiceType { - object None : ItemsChoiceType() - object SingleChoice : ItemsChoiceType() - object MultiChoice : ItemsChoiceType() -} - -context(Context) -fun UiText?.asString(): String? { - return this.parseString(this@Context) -} diff --git a/app/src/main/kotlin/com/meloda/fast/ext/Ext.kt b/app/src/main/kotlin/com/meloda/fast/ext/Ext.kt deleted file mode 100644 index 160e5890..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/Ext.kt +++ /dev/null @@ -1,148 +0,0 @@ -package com.meloda.fast.ext - -import android.content.res.Configuration -import android.content.res.Resources -import android.util.DisplayMetrics -import androidx.appcompat.app.AppCompatDelegate -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.google.common.net.MediaType -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.screens.settings.SettingsFragment -import com.meloda.fast.util.AndroidUtils -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds - -@Deprecated("use resources or rewrite in Compose") -fun Int.dpToPx(): Int { - val metrics = Resources.getSystem().displayMetrics - return (this * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt() -} - -@Deprecated("use resources or rewrite in Compose") -fun Float.dpToPx(): Int { - val metrics = Resources.getSystem().displayMetrics - return (this * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt() -} - -val MediaType.mimeType: String get() = "${type()}/${subtype()}" - -@Throws(NullPointerException::class) -fun T?.notNull(lazyMessage: (() -> Any)? = null): T { - return if (lazyMessage != null) { - requireNotNull(this, lazyMessage) - } else { - requireNotNull(this) - } -} - -inline fun Iterable.findIndex(predicate: (T) -> Boolean): Int? { - return indexOf(firstOrNull(predicate)).let { if (it == -1) null else it } -} - -inline fun > Iterable.toMap( - destination: M, - keySelector: (T) -> K, -): M { - for (element in this) { - val key = keySelector(element) - destination[key] = element - } - return destination -} - -fun MutableList.addIf(element: T, condition: () -> Boolean) { - if (condition.invoke()) add(element) -} - -context(ViewModel) -fun Flow.listenValue(action: suspend (T) -> Unit) = listenValue(viewModelScope, action) - -fun Flow.listenValue( - coroutineScope: CoroutineScope, - action: suspend (T) -> Unit -): Job = onEach(action::invoke).launchIn(coroutineScope) - -fun isSystemUsingDarkMode(): Boolean { - val nightThemeMode = AppGlobal.preferences.getInt( - SettingsFragment.KEY_APPEARANCE_DARK_THEME, - SettingsFragment.DEFAULT_VALUE_APPEARANCE_DARK_THEME - ) - val appForceDarkMode = nightThemeMode == AppCompatDelegate.MODE_NIGHT_YES - val appBatterySaver = nightThemeMode == AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY - - val systemUiNightMode = AppGlobal.resources.configuration.uiMode - - val isSystemBatterySaver = AndroidUtils.isBatterySaverOn() - val isSystemUsingDarkTheme = - systemUiNightMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES - - return appForceDarkMode || (appBatterySaver && isSystemBatterySaver) || (!appBatterySaver && isSystemUsingDarkTheme && nightThemeMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) -} - -fun createTimerFlow( - time: Int, - onStartAction: suspend () -> Unit, - onTickAction: suspend (remainedTime: Int) -> Unit, - onTimeoutAction: suspend () -> Unit, - interval: Duration = 1.seconds -): Flow = (time downTo 0) - .asSequence() - .asFlow() - .onStart { onStartAction() } - .onEach { timeLeft -> - onTickAction(timeLeft) - if (timeLeft == 0) { - onTimeoutAction() - } else { - delay(interval) - } - } - -fun createTimerFlow( - isNeedToEndCondition: suspend () -> Boolean, - onStartAction: (suspend () -> Unit)? = null, - onTickAction: (suspend () -> Unit)? = null, - onEndAction: (suspend () -> Unit)? = null, - interval: Duration = 1.seconds -): Flow = flow { - while (true) { - val isNeedToEnd = isNeedToEndCondition() - emit(isNeedToEnd) - if (isNeedToEnd) break - } -} - .onStart { onStartAction?.invoke() } - .onEach { isNeedToEnd -> - onTickAction?.invoke() - if (isNeedToEnd) { - onEndAction?.invoke() - } else { - delay(interval) - } - } - -context(ViewModel) -fun MutableSharedFlow.emitOnMainScope(value: T) = emitOnScope(value, Dispatchers.Main) - -context(ViewModel) -fun MutableSharedFlow.emitOnScope( - value: T, - dispatcher: CoroutineDispatcher = Dispatchers.Default, -) { - viewModelScope.launch(dispatcher) { - emit(value) - } -} - -context(CoroutineScope) -suspend fun MutableSharedFlow.emitWithMain(value: T) { - withContext(Dispatchers.Main) { - emit(value) - } -} - -context(ViewModel) -fun MutableStateFlow.updateValue(newValue: T) = this.update { newValue } diff --git a/app/src/main/kotlin/com/meloda/fast/ext/FragmentExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/FragmentExt.kt deleted file mode 100644 index 9663cd05..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/FragmentExt.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.meloda.fast.ext - -import android.graphics.drawable.Drawable -import android.widget.Toast -import androidx.annotation.ColorRes -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import com.meloda.fast.model.base.UiText -import com.meloda.fast.model.base.parseString -import kotlinx.coroutines.flow.Flow - -context(Fragment) -fun Flow.listenValue( - action: suspend (T) -> Unit -) = listenValue(lifecycleScope, action) - - -context(Fragment) -fun String.toast(duration: Int = Toast.LENGTH_LONG) = toast(requireContext(), duration) - -context(Fragment) -fun color(@ColorRes resId: Int): Int { - return ContextCompat.getColor(requireContext(), resId) -} - -context(Fragment) -fun drawable(@DrawableRes resId: Int): Drawable? { - return ContextCompat.getDrawable(requireContext(), resId) -} - -context(Fragment) -fun string(@StringRes resId: Int): String { - return getString(resId) -} - -context(Fragment) -fun string(@StringRes resId: Int, vararg args: Any?): String { - return getString(resId, *args) -} - -context(Fragment) -fun UiText?.asString(): String? { - return this.parseString(this@Fragment.requireContext()) -} diff --git a/app/src/main/kotlin/com/meloda/fast/ext/GlideExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/GlideExt.kt deleted file mode 100644 index 6f8c2e73..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/GlideExt.kt +++ /dev/null @@ -1,189 +0,0 @@ -package com.meloda.fast.ext - -import android.graphics.Bitmap -import android.graphics.drawable.ColorDrawable -import android.graphics.drawable.Drawable -import android.net.Uri -import android.widget.ImageView -import com.bumptech.glide.Glide -import com.bumptech.glide.Priority -import com.bumptech.glide.load.DataSource -import com.bumptech.glide.load.Transformation -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.load.engine.GlideException -import com.bumptech.glide.load.resource.bitmap.* -import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade -import com.bumptech.glide.request.RequestListener -import com.bumptech.glide.request.RequestOptions -import com.bumptech.glide.request.target.Target - -object ImageLoader { - - val userAvatarTransformations = listOf( - TypeTransformations.CircleCrop - ) - - fun ImageView.clear() { - this.setImageDrawable(null) - } - - fun ImageView.loadWithGlide(block: GlideParams.() -> Unit) { - val params = GlideParams() - block.invoke(params) - loadWithGlide(params) - } - - fun ImageView.loadWithGlide(params: GlideParams) { - val request = Glide.with(this) - - var builder = when { - params.imageUrl != null -> request.load(params.imageUrl) - params.imageUri != null -> request.load(params.imageUri) - params.drawableRes != null -> request.load(params.drawableRes) - drawable != null -> request.load(drawable) - else -> request.load(null as Drawable?) - } - - val transforms = params.transformations.toMutableList() - if (params.asCircle) { - transforms += TypeTransformations.CircleCrop - } - - builder = builder - .apply(TypeTransformations.createRequestOptions(transforms)) - .error( - params.errorDrawable - ?: if (params.errorColor != null) { - ColorDrawable(requireNotNull(params.errorColor)) - } else null - ) - .placeholder( - params.placeholderDrawable - ?: if (params.placeholderColor != null) { - ColorDrawable(requireNotNull(params.placeholderColor)) - } else null - ) - .addListener(ImageLoadRequestListener(params.onLoadedAction, params.onFailedAction)) - .addListener(ImageLoadDoneListener(params.onDoneAction)) - .diskCacheStrategy(params.cacheStrategy) - .priority(params.loadPriority) - - if (params.crossFade || params.crossFadeDuration != null) { - builder = builder.transition(withCrossFade(params.crossFadeDuration ?: 200)) - } - - builder.into(this) - } -} - -data class GlideParams( - var imageUrl: String? = null, - var imageUri: Uri? = null, - var drawableRes: Int? = null, - var imageDrawable: Drawable? = null, - var placeholderDrawable: Drawable? = null, - var placeholderColor: Int? = null, - var errorDrawable: Drawable? = placeholderDrawable, - var errorColor: Int? = null, - var crossFade: Boolean = false, - var crossFadeDuration: Int? = null, - var asCircle: Boolean = false, - var transformations: List = emptyList(), - var onLoadedAction: (() -> Unit)? = null, - var onFailedAction: (() -> Unit)? = null, - var onDoneAction: (() -> Unit)? = null, - var loadPriority: Priority = Priority.NORMAL, - var cacheStrategy: DiskCacheStrategy = DiskCacheStrategy.ALL, -) - -class ImageLoadRequestListener( - private val onLoadedAction: (() -> Unit)?, - private val onFailedAction: (() -> Unit)?, -) : RequestListener { - - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean, - ): Boolean { - onFailedAction?.invoke() - return false - } - - override fun onResourceReady( - resource: Drawable?, - model: Any?, - target: Target?, - dataSource: DataSource?, - isFirstResource: Boolean, - ): Boolean { - onLoadedAction?.invoke() - return false - } -} - -class ImageLoadDoneListener(private val onDoneAction: (() -> Unit)?) : RequestListener { - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean, - ): Boolean { - onDoneAction?.invoke() - return false - } - - override fun onResourceReady( - resource: Drawable?, - model: Any?, - target: Target?, - dataSource: DataSource?, - isFirstResource: Boolean, - ): Boolean { - onDoneAction?.invoke() - return false - } -} - -sealed class TypeTransformations { - - object CenterCrop : TypeTransformations() - - object CenterInside : TypeTransformations() - - object CircleCrop : TypeTransformations() - - class RoundedCornerCrop(val radius: Int) : TypeTransformations() - - class GranularRoundedCornerCrop( - val topLeft: Float, - val topRight: Float, - val bottomRight: Float, - val bottomLeft: Float, - ) : TypeTransformations() - - fun toGlideTransform(): Transformation = when (this) { - CenterCrop -> CenterCrop() - CenterInside -> CenterInside() - is RoundedCornerCrop -> RoundedCorners(radius) - is GranularRoundedCornerCrop -> GranularRoundedCorners( - topLeft, - topRight, - bottomRight, - bottomLeft - ) - CircleCrop -> CircleCrop() - } - - companion object { - - fun createRequestOptions(transformations: List): RequestOptions { - val mappedTransformations = transformations - .map(TypeTransformations::toGlideTransform) - .toTypedArray() - - return RequestOptions().transform(* mappedTransformations) - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/ext/NumbersExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/NumbersExt.kt deleted file mode 100644 index e8b16cb1..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/NumbersExt.kt +++ /dev/null @@ -1 +0,0 @@ -package com.meloda.fast.ext diff --git a/app/src/main/kotlin/com/meloda/fast/ext/StringExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/StringExt.kt deleted file mode 100644 index 3c615a2f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/StringExt.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.meloda.fast.ext - -import android.content.Context -import android.widget.Toast -import com.meloda.fast.model.base.UiText - -inline fun String?.ifEmpty(defaultValue: () -> String?): String? = - if (this?.isEmpty() == true) defaultValue() else this - -fun String?.orDots(count: Int = 3): String { - return this ?: ("." * count) -} - -operator fun String.times(count: Int): String { - val builder = StringBuilder() - for (i in 0 until count) { - builder.append(this) - } - - return builder.toString() -} - -fun String.toast(context: Context, duration: Int = Toast.LENGTH_LONG) { - Toast.makeText(context, this, duration).show() -} - -context (Context) -fun String.toast(duration: Int = Toast.LENGTH_LONG) = toast(this@Context, duration) - -fun String.asUiText(): UiText = UiText.Simple(this) diff --git a/app/src/main/kotlin/com/meloda/fast/ext/ViewExt.kt b/app/src/main/kotlin/com/meloda/fast/ext/ViewExt.kt deleted file mode 100644 index 4c131c38..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ext/ViewExt.kt +++ /dev/null @@ -1,196 +0,0 @@ -package com.meloda.fast.ext - -import android.content.Context -import android.graphics.Rect -import android.graphics.drawable.Drawable -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputMethodManager -import android.widget.EditText -import android.widget.ImageView -import android.widget.TextView -import androidx.annotation.ColorInt -import androidx.annotation.Px -import androidx.appcompat.widget.Toolbar -import androidx.core.view.* -import com.google.android.material.textfield.TextInputEditText -import com.google.android.material.textfield.TextInputLayout -import com.meloda.fast.R -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.databinding.ToolbarMenuItemAvatarBinding -import com.meloda.fast.ext.ImageLoader.loadWithGlide - -val EditText.trimmedText: String get() = text.toString().trim() -fun EditText.selectLast() { - setSelection(text.length) -} - -inline fun EditText.onDone(crossinline callback: () -> Unit) { - imeOptions = EditorInfo.IME_ACTION_DONE - maxLines = 1 - setOnEditorActionListener { _, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_DONE) { - callback.invoke() - return@setOnEditorActionListener true - } - false - } -} - -@Deprecated("use InsetManager") -fun View.showKeyboard(flags: Int = 0) { - (AppGlobal.Instance.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) - .showSoftInput(this, flags) -} - -@Deprecated("use InsetManager") -fun View.hideKeyboard(focusedView: View? = null, flags: Int = 0) { - (AppGlobal.Instance.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager) - .hideSoftInputFromWindow(focusedView?.windowToken ?: this.windowToken, flags) -} - -fun TextInputLayout.clearError() { - if (error != null) error = null -} - -fun TextInputLayout.toggleError(errorText: String, isNeedToShow: Boolean) { - if (isNeedToShow) { - this.error = errorText - } else { - clearError() - } -} - -fun TextInputLayout.clearTextOnErrorIconClick(textField: TextInputEditText) { - setErrorIconOnClickListener { - textField.text = null - textField.showKeyboard() - } -} - -@JvmOverloads -fun ImageView.toggleVisibilityIfHasContent(visibilityWhenFalse: Int = View.GONE) { - visibility = if (drawable != null) View.VISIBLE else visibilityWhenFalse -} - -@JvmOverloads -fun TextView.toggleVisibilityIfHasContent(visibilityWhenFalse: Int = View.GONE) { - visibility = if (!text.isNullOrEmpty()) View.VISIBLE else visibilityWhenFalse -} - -fun View.setMarginsPx( - @Px leftMargin: Int? = null, - @Px topMargin: Int? = null, - @Px rightMargin: Int? = null, - @Px bottomMargin: Int? = null, -) { - (layoutParams as? ViewGroup.MarginLayoutParams)?.let { params -> - leftMargin?.run { params.leftMargin = this } - topMargin?.run { params.topMargin = this } - rightMargin?.run { params.rightMargin = this } - bottomMargin?.run { params.bottomMargin = this } - - requestLayout() - } -} - -fun TextView.clear() { - text = null -} - -fun View.invisible() = run { isInvisible = true } -fun View.visible() = run { isVisible = true } -fun View.gone() = run { isGone = true } - -@JvmOverloads -fun View.toggleVisibility(visible: Boolean?, visibilityWhenFalse: Int = View.GONE) = - run { visibility = if (visible == true) View.VISIBLE else visibilityWhenFalse } - -fun Toolbar.tintMenuItemIcons(@ColorInt colorToTint: Int) { - menu.forEach { item -> - item.icon?.setTint(colorToTint) - } -} - -fun Toolbar.addAvatarMenuItem(urlToLoad: String? = null, drawable: Drawable? = null): MenuItem { - val avatarMenuItemBinding = ToolbarMenuItemAvatarBinding.inflate( - LayoutInflater.from(context), null, false - ) - - val avatarMenuItem = menu.add(context.getString(R.string.navigation_profile)) - avatarMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) - avatarMenuItem.actionView = avatarMenuItemBinding.root - - val imageView = avatarMenuItemBinding.avatar - - when { - urlToLoad != null -> { - imageView.loadWithGlide { - imageUrl = urlToLoad - transformations = ImageLoader.userAvatarTransformations - } - } - - drawable != null -> { - imageView.loadWithGlide { - imageDrawable = drawable - transformations = ImageLoader.userAvatarTransformations - } - } - } - - return avatarMenuItem -} - -fun View.doOnApplyWindowInsets( - block: ( - view: View, - insets: WindowInsetsCompat, - paddings: Rect, - margins: Rect - ) -> WindowInsetsCompat -) { - val initialPaddings = recordInitialPaddingsForView(this) - val initialMargins = recordInitialMarginsForView(this) - - ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets -> - block(view, insets, initialPaddings, initialMargins) - } - - requestApplyInsetsWhenAttached() -} - -private fun recordInitialPaddingsForView(view: View) = - Rect(view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom) - -private fun recordInitialMarginsForView(view: View) = - Rect(view.marginStart, view.marginTop, view.marginEnd, view.marginBottom) - -fun View.requestApplyInsetsWhenAttached() { - if (isAttachedToWindow) { - requestApplyInsets() - } else { - doOnAttach { requestApplyInsets() } - } -} - -fun EditText.updateTextIfDiffer(text: String?) { - if (this.text?.toString() == text) return - setText(text) -} - -fun ViewGroup.bulkIsEnabled(isEnabled: Boolean) { - this.isEnabled = isEnabled - toggleChildrenIsEnabled(isEnabled) -} - -fun ViewGroup.toggleChildrenIsEnabled(isEnabled: Boolean) { - children.forEach { view -> view.toggleIsEnabled(isEnabled) } -} - -fun View.toggleIsEnabled(isEnabled: Boolean) { - this.isEnabled = isEnabled -} diff --git a/app/src/main/kotlin/com/meloda/fast/model/SelectableItem.kt b/app/src/main/kotlin/com/meloda/fast/model/SelectableItem.kt deleted file mode 100644 index 1eb415c4..00000000 --- a/app/src/main/kotlin/com/meloda/fast/model/SelectableItem.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.meloda.fast.model - -import android.os.Parcelable -import androidx.room.Ignore -import kotlinx.parcelize.IgnoredOnParcel -import kotlinx.parcelize.Parcelize - -@Parcelize -open class SelectableItem : Parcelable { - - @Ignore - @IgnoredOnParcel - var isSelected: Boolean = false - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/model/UpdateItem.kt b/app/src/main/kotlin/com/meloda/fast/model/UpdateItem.kt deleted file mode 100644 index e293551f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/model/UpdateItem.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.meloda.fast.model - -import android.os.Parcelable -import com.google.gson.Gson -import kotlinx.parcelize.Parcelize - -@Parcelize -data class UpdateItem( - val id: Int, - val versionName: String, - val versionCode: Int, - val mandatory: Int, - val changelog: String?, - val enabled: Int, - val fileName: String, - val date: Long, - val extension: String, - val originalName: String, - val fileSize: Int, - val preRelease: Int, - val downloadLink: String, -) : Parcelable { - - fun isMandatory(): Boolean = mandatory == 1 - fun isEnabled(): Boolean = enabled == 1 - fun isPreRelease(): Boolean = preRelease == 1 - - override fun toString(): String { - return Gson().toJson(this) - } - - companion object { - val EMPTY = UpdateItem( - id = 0, - versionName = "1.0.0", - versionCode = 2, - mandatory = 1, - changelog = "Some kind of simple changelog", - enabled = 1, - fileName = "bruhmeme.apk", - date = System.currentTimeMillis(), - extension = "", - originalName = "", - fileSize = 0, - preRelease = 0, - downloadLink = "https://c4.kemono.party/data/98/8c/988cf166f1ee9cd318e2407e6bfbabf60bffa53ed229ea0b2434009f1598e039.png?f=JessieGym002b4pt.png" - ) - } - -} - -@Parcelize -data class UpdateActualUrl(val url: String) : Parcelable diff --git a/app/src/main/kotlin/com/meloda/fast/model/base/AdapterDiffItem.kt b/app/src/main/kotlin/com/meloda/fast/model/base/AdapterDiffItem.kt deleted file mode 100644 index 015fde04..00000000 --- a/app/src/main/kotlin/com/meloda/fast/model/base/AdapterDiffItem.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.meloda.fast.model.base - -interface AdapterDiffItem { - - val id: Int - - fun areItemsTheSame(newItem: AdapterDiffItem): Boolean { - return id == newItem.id - } - - fun areContentsTheSame(newItem: AdapterDiffItem): Boolean { - return this == newItem - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/model/base/DisplayableItem.kt b/app/src/main/kotlin/com/meloda/fast/model/base/DisplayableItem.kt deleted file mode 100644 index 96878c4e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/model/base/DisplayableItem.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.meloda.fast.model.base - -interface DisplayableItem diff --git a/app/src/main/kotlin/com/meloda/fast/model/base/UiImage.kt b/app/src/main/kotlin/com/meloda/fast/model/base/UiImage.kt deleted file mode 100644 index f5042622..00000000 --- a/app/src/main/kotlin/com/meloda/fast/model/base/UiImage.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.meloda.fast.model.base - -import android.content.Context -import android.graphics.drawable.ColorDrawable -import android.graphics.drawable.Drawable -import android.widget.ImageView -import androidx.annotation.ColorInt -import androidx.annotation.ColorRes -import androidx.annotation.DrawableRes -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import androidx.core.content.ContextCompat -import com.meloda.fast.ext.GlideParams -import com.meloda.fast.ext.ImageLoader.loadWithGlide - -sealed class UiImage { - - data class Resource(@DrawableRes val resId: Int) : UiImage() - - data class Simple(val drawable: Drawable?) : UiImage() - - data class Color(@ColorInt val color: Int) : UiImage() - - data class ColorResource(@ColorRes val resId: Int) : UiImage() - - data class Url(val url: String) : UiImage() - - fun extractUrl(): String? = when (this) { - is Url -> this.url - else -> null - } - - fun getResourceId(): Int? = when(this) { - is Resource -> this.resId - else -> null - } -} - -fun ImageView.setImage(image: UiImage, glideBlock: GlideParams.() -> Unit) { - val glideParams = GlideParams() - glideBlock.invoke(glideParams) - this.setImage(image, glideParams) -} - -fun ImageView.setImage(image: UiImage, glideParams: GlideParams? = null) { - image.attachTo(this, glideParams) -} - -fun UiImage?.attachTo(imageView: ImageView, glideBlock: GlideParams.() -> Unit) { - val glideParams = GlideParams() - glideBlock.invoke(glideParams) - this.attachTo(imageView, glideParams) -} - -fun UiImage?.attachTo(imageView: ImageView, glideParams: GlideParams? = null) { - when (this) { - is UiImage.Simple -> imageView.setImageDrawable(drawable) - is UiImage.Resource -> imageView.setImageResource(resId) - is UiImage.Color -> imageView.setImageDrawable(ColorDrawable(color)) - is UiImage.ColorResource -> imageView.setImageDrawable( - ColorDrawable(ContextCompat.getColor(imageView.context, resId)) - ) - - is UiImage.Url -> glideParams?.let { params -> - params.imageUrl = url - imageView.loadWithGlide(params) - } - - else -> Unit - } -} - -fun UiImage?.asDrawable(context: Context): Drawable? { - return when (this) { - is UiImage.Simple -> drawable - is UiImage.Resource -> ContextCompat.getDrawable(context, resId) - is UiImage.Color -> ColorDrawable(color) - is UiImage.ColorResource -> ColorDrawable(ContextCompat.getColor(context, resId)) - else -> null - } -} - -@Composable -fun UiImage?.getImage(): Any? { - val context = LocalContext.current - return when(this) { - is UiImage.Color -> ColorDrawable(color) - is UiImage.ColorResource -> ColorDrawable(ContextCompat.getColor(context, resId)) - is UiImage.Resource -> ContextCompat.getDrawable(context, resId) - is UiImage.Simple -> drawable - is UiImage.Url -> url - null -> null - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/receiver/StopLongPollServiceReceiver.kt b/app/src/main/kotlin/com/meloda/fast/receiver/StopLongPollServiceReceiver.kt deleted file mode 100644 index 6379f645..00000000 --- a/app/src/main/kotlin/com/meloda/fast/receiver/StopLongPollServiceReceiver.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.meloda.fast.receiver - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import androidx.core.app.NotificationManagerCompat -import androidx.core.content.edit -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.screens.main.activity.LongPollState -import com.meloda.fast.screens.main.activity.MainActivity -import com.meloda.fast.screens.settings.SettingsFragment -import kotlinx.coroutines.flow.update - -class StopLongPollServiceReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == ACTION_STOP) { - val notificationId = intent.getIntExtra(NOTIFICATION_ID, -1) - - if (notificationId != -1) { - NotificationManagerCompat.from(context).cancel(notificationId) - } - - AppGlobal.preferences.edit { - putBoolean(SettingsFragment.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, false) - } - - MainActivity.longPollState.update { LongPollState.Stop } - MainActivity.longPollState.update { LongPollState.DefaultService } - } - } - - companion object { - const val ACTION_STOP = "stop_long_poll" - const val NOTIFICATION_ID = "notification_id" - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/captcha/CaptchaScreens.kt b/app/src/main/kotlin/com/meloda/fast/screens/captcha/CaptchaScreens.kt deleted file mode 100644 index e6542189..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/captcha/CaptchaScreens.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.screens.captcha - -import com.github.terrakok.cicerone.androidx.FragmentScreen -import com.meloda.fast.screens.captcha.presentation.CaptchaFragment - -object CaptchaScreens { - - fun captchaScreen() = FragmentScreen(key = "CaptchaScreen") { - CaptchaFragment.newInstance() - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/captcha/di/CaptchaDI.kt b/app/src/main/kotlin/com/meloda/fast/screens/captcha/di/CaptchaDI.kt deleted file mode 100644 index 90de93a8..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/captcha/di/CaptchaDI.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.meloda.fast.screens.captcha.di - -import com.meloda.fast.di.navigationModule -import com.meloda.fast.screens.captcha.presentation.CaptchaCoordinator -import com.meloda.fast.screens.captcha.presentation.CaptchaCoordinatorImpl -import com.meloda.fast.screens.captcha.presentation.CaptchaViewModelImpl -import com.meloda.fast.screens.captcha.screen.CaptchaScreen -import com.meloda.fast.screens.captcha.validation.CaptchaValidator -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.core.module.dsl.singleOf -import org.koin.core.qualifier.named -import org.koin.core.scope.Scope -import org.koin.dsl.bind -import org.koin.dsl.module - -val captchaModule = module { - val moduleQualifier = named("captcha") - - includes(navigationModule) - - single(moduleQualifier) { screen().resultFlow } - single { screen().getArguments() } - - single { - CaptchaCoordinatorImpl( - resultFlow = get(moduleQualifier), - router = get() - ) - } bind CaptchaCoordinator::class - - singleOf(::CaptchaValidator) - viewModelOf(::CaptchaViewModelImpl) -} - -private fun Scope.screen(): CaptchaScreen = get() - diff --git a/app/src/main/kotlin/com/meloda/fast/screens/captcha/model/CaptchaValidationResult.kt b/app/src/main/kotlin/com/meloda/fast/screens/captcha/model/CaptchaValidationResult.kt deleted file mode 100644 index 6fddec93..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/captcha/model/CaptchaValidationResult.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.meloda.fast.screens.captcha.model - -sealed class CaptchaValidationResult { - object Empty : CaptchaValidationResult() - object Valid : CaptchaValidationResult() - - fun isValid() = this == Valid -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/captcha/presentation/CaptchaCoordinator.kt b/app/src/main/kotlin/com/meloda/fast/screens/captcha/presentation/CaptchaCoordinator.kt deleted file mode 100644 index 45164b7e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/captcha/presentation/CaptchaCoordinator.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.meloda.fast.screens.captcha.presentation - -import com.github.terrakok.cicerone.Router -import com.meloda.fast.screens.captcha.screen.CaptchaResult -import kotlinx.coroutines.flow.MutableSharedFlow - -interface CaptchaCoordinator { - - fun finishWithResult(result: CaptchaResult) -} - -class CaptchaCoordinatorImpl constructor( - val resultFlow: MutableSharedFlow, - val router: Router -) : CaptchaCoordinator { - - override fun finishWithResult(result: CaptchaResult) { - resultFlow.tryEmit(result) - router.exit() - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/captcha/presentation/CaptchaFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/captcha/presentation/CaptchaFragment.kt deleted file mode 100644 index 292da0f2..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/captcha/presentation/CaptchaFragment.kt +++ /dev/null @@ -1,234 +0,0 @@ -package com.meloda.fast.screens.captcha.presentation - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.addCallback -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import coil.request.ImageRequest -import com.meloda.fast.R -import com.meloda.fast.base.BaseFragment -import com.meloda.fast.screens.captcha.model.CaptchaScreenState -import com.meloda.fast.ui.* -import com.meloda.fast.ui.widgets.CoilImage -import com.meloda.fast.ui.widgets.TextFieldErrorText -import org.koin.androidx.viewmodel.ext.android.viewModel - -class CaptchaFragment : BaseFragment() { - - private val viewModel: CaptchaViewModel by viewModel() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - AppTheme { - Surface( - color = MaterialTheme.colorScheme.background, - modifier = Modifier - .statusBarsPadding() - .navigationBarsPadding() - .imePadding() - ) { - val state by viewModel.screenState.collectAsStateWithLifecycle() - - CaptchaScreen( - onCancelButtonClicked = viewModel::onCancelButtonClicked, - onCodeInputChanged = viewModel::onCodeInputChanged, - onTextFieldDoneClicked = viewModel::onTextFieldDoneClicked, - onDoneButtonClicked = viewModel::onDoneButtonClicked, - state = state - ) - } - } - } - } - - @Preview - @Composable - fun CaptchaScreenPreview() { - AppTheme { - Surface(color = MaterialTheme.colorScheme.background) { - CaptchaScreen( - onCancelButtonClicked = {}, - onCodeInputChanged = {}, - onTextFieldDoneClicked = {}, - onDoneButtonClicked = {}, - state = CaptchaScreenState.EMPTY - ) - } - } - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun CaptchaScreen( - onCancelButtonClicked: () -> Unit, - onCodeInputChanged: (String) -> Unit, - onTextFieldDoneClicked: () -> Unit, - onDoneButtonClicked: () -> Unit, - state: CaptchaScreenState, - ) { - val focusManager = LocalFocusManager.current - - Column( - modifier = Modifier - .fillMaxSize() - .padding(30.dp), - verticalArrangement = Arrangement.SpaceBetween - ) { - ExtendedFloatingActionButton( - onClick = onCancelButtonClicked, - text = { - Text( - text = "Cancel", - color = MaterialTheme.colorScheme.primary - ) - }, - icon = { - Icon( - painter = painterResource(id = R.drawable.ic_round_close_24), - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - ) - } - ) - - Column( - modifier = Modifier.fillMaxWidth(), - ) { - Text( - text = "Captcha", - style = MaterialTheme.typography.displayMedium, - color = MaterialTheme.colorScheme.onBackground - ) - Spacer(modifier = Modifier.height(38.dp)) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = "To proceed with your action, enter a code from the picture", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.weight(0.5f) - ) - Spacer(modifier = Modifier.width(24.dp)) - - CoilImage( - model = ImageRequest.Builder(LocalContext.current) - .data(state.captchaImage) - .crossfade(true) - .build(), - contentDescription = null, - modifier = Modifier - .border( - 2.dp, - MaterialTheme.colorScheme.primary, - shape = RoundedCornerShape(10.dp) - ) - .clip(RoundedCornerShape(10.dp)) - .height(48.dp) - .width(130.dp), - contentScale = ContentScale.FillBounds, - previewPainter = painterResource(id = R.drawable.test_captcha) - ) - } - - Spacer(modifier = Modifier.height(30.dp)) - - var code by remember { mutableStateOf(TextFieldValue(state.captchaCode)) } - val showError = state.codeError - - TextField( - value = code, - onValueChange = { newText -> - code = newText - onCodeInputChanged(newText.text) - }, - label = { Text(text = "Code") }, - placeholder = { Text(text = "Code") }, - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)), - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.round_qr_code_24), - contentDescription = null, - tint = if (showError) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.primary - } - ) - }, - shape = RoundedCornerShape(10.dp), - keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions( - onDone = { - focusManager.clearFocus() - onTextFieldDoneClicked() - } - ), - isError = showError - ) - - AnimatedVisibility(visible = showError) { - TextFieldErrorText(text = "Field must not be empty") - } - } - - FloatingActionButton( - onClick = onDoneButtonClicked, - containerColor = MaterialTheme.colorScheme.secondaryContainer, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) { - Icon( - painter = painterResource(id = R.drawable.ic_round_done_24), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSecondaryContainer - ) - } - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - activity?.onBackPressedDispatcher?.addCallback { - viewModel.onBackButtonClicked() - } - } - - companion object { - - fun newInstance(): CaptchaFragment { - return CaptchaFragment() - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaArguments.kt b/app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaArguments.kt deleted file mode 100644 index 751f2223..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaArguments.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.meloda.fast.screens.captcha.screen - -data class CaptchaArguments(val captchaSid: String, val captchaImage: String) diff --git a/app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaResult.kt b/app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaResult.kt deleted file mode 100644 index 1504a66e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaResult.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.meloda.fast.screens.captcha.screen - -sealed class CaptchaResult { - object Cancelled : CaptchaResult() - data class Success(val sid: String, val code: String) : CaptchaResult() -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaScreen.kt b/app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaScreen.kt deleted file mode 100644 index 7aeca346..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/captcha/screen/CaptchaScreen.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.meloda.fast.screens.captcha.screen - -import com.github.terrakok.cicerone.Router -import com.meloda.fast.base.screen.AppScreen -import com.meloda.fast.base.screen.createResultFlow -import com.meloda.fast.screens.captcha.CaptchaScreens -import kotlin.properties.Delegates - -class CaptchaScreen : AppScreen { - - override val resultFlow = createResultFlow() - - override var args: CaptchaArguments by Delegates.notNull() - - override fun show(router: Router, args: CaptchaArguments) { - this.args = args - router.navigateTo(CaptchaScreens.captchaScreen()) - } -} - - diff --git a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoFragment.kt deleted file mode 100644 index 1131d301..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoFragment.kt +++ /dev/null @@ -1,299 +0,0 @@ -package com.meloda.fast.screens.chatinfo - -import android.os.Bundle -import android.view.View -import android.widget.Toast -import androidx.core.os.bundleOf -import androidx.fragment.app.Fragment -import by.kirich1409.viewbindingdelegate.viewBinding -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.tabs.TabLayoutMediator -import com.meloda.fast.R -import com.meloda.fast.api.model.VkChat -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.model.domain.VkConversationDomain -import com.meloda.fast.base.viewmodel.BaseViewModelFragment -import com.meloda.fast.base.viewmodel.VkEvent -import com.meloda.fast.databinding.FragmentChatInfoBinding -import com.meloda.fast.ext.ImageLoader.loadWithGlide -import com.meloda.fast.ext.getParcelableCompat -import com.meloda.fast.ext.gone -import com.meloda.fast.ext.orDots -import com.meloda.fast.ext.visible -import com.meloda.fast.screens.messages.MessagesHistoryFragment -import dev.chrisbanes.insetter.applyInsetter -import org.koin.androidx.viewmodel.ext.android.viewModel -import java.text.SimpleDateFormat -import java.util.Locale - -class ChatInfoFragment : BaseViewModelFragment(R.layout.fragment_chat_info) { - - companion object { - const val KeyConfirmRemoveChatUser = "confirm_remove_chat_user" - const val KeyRemoveChatUser = "remove_chat_user" - const val ArgMemberId = "member_id" - - private const val ArgConversation = "conversation" - private const val ArgUser = "user" - private const val ArgGroup = "group" - - fun newInstance( - conversation: VkConversationDomain, - user: VkUser?, - group: VkGroup?, - ): ChatInfoFragment { - val fragment = ChatInfoFragment() - fragment.arguments = bundleOf( - ArgConversation to conversation, - ArgUser to user, - ArgGroup to group - ) - - return fragment - } - } - - override val viewModel: ChatInfoViewModel by viewModel() - - private val binding by viewBinding(FragmentChatInfoBinding::bind) - - private val user: VkUser? by lazy { - requireArguments().getParcelableCompat(MessagesHistoryFragment.ARG_USER, VkUser::class.java) - } - - private val group: VkGroup? by lazy { - requireArguments().getParcelableCompat( - MessagesHistoryFragment.ARG_GROUP, - VkGroup::class.java - ) - } - - private val conversation: VkConversationDomain by lazy { - requireNotNull( - requireArguments().getParcelableCompat( - MessagesHistoryFragment.ARG_CONVERSATION, - VkConversationDomain::class.java - ) - ) - } - - private val chatProfiles: MutableList = mutableListOf() - private val chatGroups: MutableList = mutableListOf() - private val chatMembers: MutableList = mutableListOf() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewModel.getConversationMembers(conversation.id) - - val title = when { - conversation.isChat() -> conversation.conversationTitle - conversation.isUser() -> user?.toString() - conversation.isGroup() -> group?.name - else -> null - } - - - binding.toolbar.applyInsetter { - type(statusBars = true) { padding() } - } - binding.progresBar.applyInsetter { - type(navigationBars = true) { padding() } - } - binding.toolbar.title = title.orDots() - - updateStatus() - - val avatar = when { - conversation.isUser() -> user?.photo200 - conversation.isGroup() -> group?.photo200 - conversation.isChat() -> conversation.conversationPhoto - else -> null - } - - val avatarImageView = binding.toolbar.avatarImageView - avatarImageView.visible() - avatarImageView.loadWithGlide { - imageUrl = avatar - asCircle = true - crossFade = true - } - - binding.toolbar.avatarClickAction = { - showAvatarOptions() - } - binding.toolbar.startButtonClickAction = - { requireActivity().onBackPressedDispatcher.onBackPressed() } - - binding.viewPager.offscreenPageLimit = getTabsCount() - 1 - - childFragmentManager.setFragmentResultListener( - KeyConfirmRemoveChatUser, - this - ) { _, bundle -> - val memberId = bundle.getInt(ArgMemberId) - showConfirmRemoveMemberAlert(memberId) - } - } - - override fun onEvent(event: VkEvent) { - super.onEvent(event) - - when (event) { - is GetConversationMembersEvent -> { - fillChatInfo(event) - } - - is RemoveChatUserEvent -> { - val memberId = event.memberId - childFragmentManager.setFragmentResult( - KeyRemoveChatUser, bundleOf( - ArgMemberId to memberId - ) - ) - } - } - } - - // TODO: 17.04.2023, Danil Nikolaev: handle loading - private fun onProgressStart() { - binding.tabs.gone() - binding.viewPager.gone() - binding.progresBar.visible() - } - - private fun onProgressStop() { - binding.tabs.visible() - binding.viewPager.visible() - binding.progresBar.gone() - } - - private fun fillChatInfo(event: GetConversationMembersEvent) { - val onlineMembers = event.profiles.filter { it.online } - updateStatus(onlineMembers.size) - - val eventChatMembers = event.items.map { vkChatMember -> - val memberUser: VkUser? = if (vkChatMember.memberId < 0) null - else event.profiles.firstOrNull { it.id == vkChatMember.memberId } - - val memberGroup: VkGroup? = if (vkChatMember.memberId > 0) null - else event.groups.firstOrNull { it.id == vkChatMember.memberId } - - VkChat.ChatMember( - id = vkChatMember.memberId, - type = if (vkChatMember.memberId > 0) VkChat.ChatMember.ChatMemberType.Profile else VkChat.ChatMember.ChatMemberType.Group, - isOnline = memberUser?.online, - lastSeen = memberUser?.lastSeen, - name = memberGroup?.name, - firstName = memberUser?.firstName, - lastName = memberUser?.lastName, - invitedBy = vkChatMember.invitedBy, - photo50 = null, - photo100 = null, - photo200 = memberUser?.photo200 ?: memberGroup?.photo200, - isOwner = vkChatMember.isOwner, - isAdmin = vkChatMember.isAdmin, - canKick = vkChatMember.canKick - ) - } - - chatProfiles.addAll(event.profiles) - chatGroups.addAll(event.groups) - chatMembers.addAll(eventChatMembers) - prepareTabs() - } - - private fun updateStatus(onlineMembersCount: Int? = null) { - val status = when { - conversation.isChat() -> { - val membersCountText = "${conversation.membersCount} members" - if (onlineMembersCount == null) membersCountText - else { - "$membersCountText, $onlineMembersCount online" - } - } - - conversation.isUser() -> when { - // TODO: 9/15/2021 user normal time - user?.online == true -> "Online" - user?.lastSeen != null -> "Last seen at ${ - SimpleDateFormat( - "HH:mm", - Locale.getDefault() - ).format(user?.lastSeen!! * 1000L) - }" - - else -> if (user?.lastSeenStatus != null) "Last seen ${user?.lastSeenStatus!!}" else "Last seen recently" - } - - conversation.isGroup() -> if (group?.membersCount != null) "${group?.membersCount} members" else "Group" - else -> null - } - - binding.toolbar.subtitle = status.orDots() - - } - - fun getTabsCount(): Int { - return if (conversation.isChat()) 6 else 5 - } - - fun createTabFragment(position: Int): Fragment { - if (conversation.isChat() && position == 0) { - return ChatInfoMembersFragment.newInstance( - chatProfiles, - chatGroups, - chatMembers - ) - } - - return Fragment() - } - - private fun prepareTabs() { - val titles = mutableListOf("Members", "Photos", "Videos", "Audios", "Files", "Links") - - if (!conversation.isChat()) { - titles.removeAt(0) - } - - binding.viewPager.adapter = ChatInfoPagerAdapter(this) - - TabLayoutMediator(binding.tabs, binding.viewPager) { tab, position -> - tab.text = titles[position] - }.attach() - } - - private fun showConfirmRemoveMemberAlert(memberId: Int) { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.warning) - .setMessage(R.string.confirm_remove_chat_user) - .setPositiveButton(R.string.yes) { _, _ -> - viewModel.removeChatUser(conversation.localId, memberId) - } - .setNegativeButton(R.string.no, null) - .show() - } - - private fun showAvatarOptions() { - val options = mutableListOf("Open") - - if (conversation.canChangeInfo) { - options += listOf("Edit", "Delete") - } - - MaterialAlertDialogBuilder(requireContext()) - .setItems(options.toTypedArray()) { _, which -> - when (options[which]) { - "Open" -> { - Toast.makeText(requireContext(), "Open photo", Toast.LENGTH_SHORT).show() - } - - else -> - Toast.makeText(requireContext(), "Change info", Toast.LENGTH_SHORT).show() - } - } - .show() - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoMembersAdapter.kt b/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoMembersAdapter.kt deleted file mode 100644 index b9a45f04..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoMembersAdapter.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.meloda.fast.screens.chatinfo - -import android.content.Context -import android.content.res.ColorStateList -import android.graphics.Color -import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.DiffUtil -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.model.VkChat -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.base.adapter.BaseAdapter -import com.meloda.fast.base.adapter.BaseHolder -import com.meloda.fast.databinding.ItemChatMemberBinding -import com.meloda.fast.ext.ImageLoader.loadWithGlide -import com.meloda.fast.ext.toggleVisibility -import java.text.SimpleDateFormat -import java.util.Locale -import java.util.Objects - -class ChatInfoMembersAdapter( - context: Context, - preAddedValues: List, - private val profiles: List, - private val groups: List, - private val confirmRemoveMemberAction: ((memberId: Int) -> Unit)? = null, -) : BaseAdapter( - context, - comparator, - preAddedValues -) { - - companion object { - val comparator = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: VkChat.ChatMember, - newItem: VkChat.ChatMember, - ): Boolean { - return oldItem.id == newItem.id - } - - override fun areContentsTheSame( - oldItem: VkChat.ChatMember, - newItem: VkChat.ChatMember, - ): Boolean { - return Objects.deepEquals(oldItem, newItem) - } - - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { - return Holder(ItemChatMemberBinding.inflate(inflater, parent, false)) - } - - inner class Holder( - private val binding: ItemChatMemberBinding, - ) : BaseHolder(binding.root) { - - private val colorOnBackground = ContextCompat.getColor(context, R.color.colorOnBackground) - private val colorPrimary = ContextCompat.getColor(context, R.color.colorPrimary) - - override fun bind(position: Int) { - val chatMember = getItem(position) - - binding.avatar.loadWithGlide { - imageUrl = chatMember.photo200 - crossFade = true - placeholderColor = Color.GRAY - errorColor = Color.RED - } - - val title = chatMember.name ?: "${chatMember.firstName} ${chatMember.lastName}" - binding.title.text = title - - binding.online.toggleVisibility(chatMember.isProfile()) - binding.online.text = - if (chatMember.isOnline == true) "Online" - else if (chatMember.lastSeen != null) "Last seen at ${ - SimpleDateFormat( - "HH:mm", - Locale.getDefault() - ).format(chatMember.lastSeen * 1000L) - }" - else "Offline" - - binding.star.toggleVisibility(chatMember.isAdmin || chatMember.isOwner) - binding.star.imageTintList = - ColorStateList.valueOf( - if (chatMember.isOwner) colorPrimary - else colorOnBackground - ) - - binding.remove.toggleVisibility( - chatMember.canKick || chatMember.id == UserConfig.userId - ) - binding.remove.setOnClickListener { confirmRemoveMemberAction?.invoke(chatMember.id) } - } - } - - fun searchMemberIndex(memberId: Int): Int? { - for (i in indices) { - val member = getItem(i) - if (member.id == memberId) return i - } - - return null - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoMembersFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoMembersFragment.kt deleted file mode 100644 index 5c54c835..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoMembersFragment.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.meloda.fast.screens.chatinfo - -import android.os.Bundle -import android.view.View -import androidx.core.os.bundleOf -import androidx.fragment.app.setFragmentResult -import androidx.fragment.app.setFragmentResultListener -import by.kirich1409.viewbindingdelegate.viewBinding -import com.meloda.fast.R -import com.meloda.fast.api.model.VkChat -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.base.BaseFragment -import com.meloda.fast.databinding.FragmentChatInfoMembersBinding -import com.meloda.fast.ext.dpToPx -import com.meloda.fast.view.SpaceItemDecoration -import dev.chrisbanes.insetter.applyInsetter - -class ChatInfoMembersFragment : BaseFragment(R.layout.fragment_chat_info_members) { - - companion object { - - private const val ArgProfiles = "profiles" - private const val ArgGroups = "groups" - private const val ArgMembers = "members" - - fun newInstance( - profiles: List, - groups: List, - members: List - ): ChatInfoMembersFragment { - val fragment = ChatInfoMembersFragment() - fragment.arguments = bundleOf( - ArgProfiles to profiles, - ArgGroups to groups, - ArgMembers to members - ) - - return fragment - } - } - - private val binding by viewBinding(FragmentChatInfoMembersBinding::bind) - - @Suppress("UNCHECKED_CAST") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.recyclerView.applyInsetter { - type(navigationBars = true) { padding() } - } - binding.recyclerView.addItemDecoration(SpaceItemDecoration(topMargin = 8.dpToPx())) - - // TODO: 17.04.2023, Danil Nikolaev: use single data class as parcelable - val profiles = requireArguments().getSerializable(ArgProfiles) as List - val groups = requireArguments().getSerializable(ArgGroups) as List - val members = requireArguments().getSerializable(ArgMembers) as List - - val adapter = - ChatInfoMembersAdapter( - requireContext(), - members, - profiles, - groups, - confirmRemoveMemberAction = { memberId -> - setFragmentResult( - ChatInfoFragment.KeyConfirmRemoveChatUser, - bundleOf(ChatInfoFragment.ArgMemberId to memberId) - ) - } - ) - binding.recyclerView.adapter = adapter - binding.recyclerView.itemAnimator = null - - setFragmentResultListener(ChatInfoFragment.KeyRemoveChatUser) { _, bundle -> - val memberId = bundle.getInt(ChatInfoFragment.ArgMemberId) - adapter.searchMemberIndex(memberId)?.let { index -> - adapter.removeAt(index) - } - } - } - -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoPagerAdapter.kt b/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoPagerAdapter.kt deleted file mode 100644 index a93a7050..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoPagerAdapter.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.meloda.fast.screens.chatinfo - -import androidx.fragment.app.Fragment -import androidx.viewpager2.adapter.FragmentStateAdapter - -class ChatInfoPagerAdapter( - private val fragment: ChatInfoFragment -) : FragmentStateAdapter(fragment) { - - override fun getItemCount(): Int { - return fragment.getTabsCount() - } - - override fun createFragment(position: Int): Fragment { - return fragment.createTabFragment(position) - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoViewModel.kt deleted file mode 100644 index 3bf2e76c..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/ChatInfoViewModel.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.meloda.fast.screens.chatinfo - -import androidx.lifecycle.viewModelScope -import com.meloda.fast.api.VKConstants -import com.meloda.fast.api.model.VkChatMember -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.base.viewmodel.DeprecatedBaseViewModel -import com.meloda.fast.base.viewmodel.VkEvent -import com.meloda.fast.data.messages.MessagesRepository -import kotlinx.coroutines.launch - -class ChatInfoViewModel constructor( - private val messagesRepository: MessagesRepository -) : DeprecatedBaseViewModel() { - - fun getConversationMembers(peerId: Int) = viewModelScope.launch { - makeJob( - { - messagesRepository.getConversationMembers( - peerId, - extended = true, - fields = VKConstants.ALL_FIELDS - ) - }, - onAnswer = { - val response = it.response ?: return@makeJob - - val items = response.items.map { member -> member.asVkChatMember() } - val profiles = response.profiles.orEmpty().map { profile -> profile.mapToDomain() } - val groups = response.groups.orEmpty().map { group -> group.mapToDomain() } - - sendEvent(GetConversationMembersEvent(response.count, items, profiles, groups)) - } - ) - } - - fun removeChatUser(chatId: Int, memberId: Int) = viewModelScope.launch { - makeJob( - { messagesRepository.removeChatUser(chatId, memberId) }, - onAnswer = { - sendEvent(RemoveChatUserEvent(chatId, memberId)) - } - ) - } - -} - -data class GetConversationMembersEvent( - val count: Int, - val items: List, - val profiles: List, - val groups: List -) : VkEvent() - -data class RemoveChatUserEvent( - val chatId: Int, val memberId: Int -) : VkEvent() diff --git a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/di/ChatInfoDI.kt b/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/di/ChatInfoDI.kt deleted file mode 100644 index ac1f6db7..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/chatinfo/di/ChatInfoDI.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.screens.chatinfo.di - -import com.meloda.fast.screens.chatinfo.ChatInfoViewModel -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.dsl.module - -val chatInfoModule = module { - viewModelOf(::ChatInfoViewModel) -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationCompose.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationCompose.kt deleted file mode 100644 index 1a094176..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationCompose.kt +++ /dev/null @@ -1,256 +0,0 @@ -package com.meloda.fast.screens.conversations - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.surfaceColorAtElevation -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.model.presentation.VkConversationUi -import com.meloda.fast.ext.combinedClickableSound -import com.meloda.fast.ext.getString -import com.meloda.fast.ext.orDots -import com.meloda.fast.model.base.getImage -import com.meloda.fast.ui.widgets.CoilImage - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun Conversation( - onItemClick: (VkConversationUi) -> Unit, - onItemLongClick: (VkConversationUi) -> Unit, - conversation: VkConversationUi, - maxLines: Int -) { - Box( - modifier = Modifier - .fillMaxWidth() - .combinedClickableSound( - onClick = { onItemClick(conversation) }, - onLongClick = { onItemLongClick(conversation) } - ) - ) { - if (conversation.isUnread) { - Box( - modifier = Modifier - .matchParentSize() - .padding(start = 8.dp) - .clip( - RoundedCornerShape( - topStart = 34.dp, - bottomStart = 34.dp - ) - ) - .background(MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp)) - ) - } - - Column(modifier = Modifier.fillMaxWidth()) { - Spacer(modifier = Modifier.height(8.dp)) - Row(modifier = Modifier.fillMaxWidth()) { - Spacer(modifier = Modifier.width(16.dp)) - Box(modifier = Modifier.size(56.dp)) { - - if (conversation.id == UserConfig.userId) { - Box( - modifier = Modifier - .fillMaxSize() - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primary) - ) { - Image( - modifier = Modifier - .align(Alignment.Center) - .size(32.dp), - painter = painterResource(id = R.drawable.ic_round_bookmark_border_24), - contentDescription = null - ) - } - } else { - Image( - modifier = Modifier - .fillMaxSize() - .clip(CircleShape), - painter = painterResource(id = R.drawable.ic_account_circle_cut), - contentDescription = null, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.outline) - ) - CoilImage( - modifier = Modifier - .fillMaxSize() - .clip(CircleShape), - contentDescription = null, - model = conversation.avatar.getImage(), - previewPainter = painterResource(id = R.drawable.ic_account_circle_cut), - ) - } - - if (conversation.isPinned) { - Box( - modifier = Modifier - .clip(CircleShape) - .defaultMinSize(minWidth = 20.dp, minHeight = 20.dp) - .background(MaterialTheme.colorScheme.outline) - ) { - Image( - modifier = Modifier - .height(14.dp) - .align(Alignment.Center), - painter = painterResource(id = R.drawable.ic_round_push_pin_24), - contentDescription = null - ) - } - } - - if (conversation.isOnline) { - Box( - modifier = Modifier - .clip(CircleShape) - .size(18.dp) - .background( - if (conversation.isUnread) { - MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp) - } else { - MaterialTheme.colorScheme.background - } - ) - .padding(2.dp) - .align(Alignment.BottomEnd) - ) { - Box( - modifier = Modifier - .clip(CircleShape) - .matchParentSize() - .background(MaterialTheme.colorScheme.primary) - ) - } - } - - if (conversation.isBirthday) { - Box( - modifier = Modifier - .clip(CircleShape) - .size(16.dp) - .background( - if (conversation.isUnread) { - MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp) - } else { - MaterialTheme.colorScheme.background - } - ) - .padding(2.dp) - .align(Alignment.TopEnd) - ) { - Box( - modifier = Modifier - .clip(CircleShape) - .matchParentSize() - .background(Color(0xFFB00B69)) - ) { - Image( - modifier = Modifier - .align(Alignment.Center) - .size(10.dp), - painter = painterResource(id = R.drawable.round_cake_24), - contentDescription = null - ) - } - } - } - } - - Spacer(modifier = Modifier.width(16.dp)) - - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = conversation.title.getString().orDots(), - modifier = Modifier, - minLines = 1, - maxLines = maxLines, - style = MaterialTheme.typography.headlineSmall.copy(fontSize = 20.sp) - ) - - Row { - conversation.attachmentImage?.getResourceId()?.let { resId -> - Column { - Spacer(modifier = Modifier.height(4.dp)) - Image( - modifier = Modifier.size(14.dp), - painter = painterResource(id = resId), - contentDescription = null, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer) - ) - } - - Spacer(modifier = Modifier.width(2.dp)) - } - - Text( - modifier = Modifier.weight(1f), - text = conversation.message, - minLines = 1, - maxLines = maxLines, - style = MaterialTheme.typography.bodyLarge, - overflow = TextOverflow.Ellipsis - ) - } - } - - Spacer(modifier = Modifier.width(4.dp)) - Column { - Text(text = conversation.date) - - conversation.unreadCount?.let { count -> - Spacer(modifier = Modifier.height(6.dp)) - Box( - modifier = Modifier - .clip(CircleShape) - .defaultMinSize(minWidth = 20.dp, minHeight = 20.dp) - .background(MaterialTheme.colorScheme.primary) - .align(Alignment.CenterHorizontally) - ) { - Text( - modifier = Modifier - .padding(2.dp) - .align(Alignment.Center), - text = count, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onPrimary - ) - } - } - } - - Spacer(modifier = Modifier.width(24.dp)) - } - - Spacer(modifier = Modifier.height(8.dp)) - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt deleted file mode 100644 index 60779e60..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt +++ /dev/null @@ -1,342 +0,0 @@ -package com.meloda.fast.screens.conversations - -import android.os.Bundle -import android.view.HapticFeedbackConstants -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.slideIn -import androidx.compose.animation.slideOut -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.MoreVert -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LargeTopAppBar -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberTopAppBarState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.meloda.fast.R -import com.meloda.fast.api.model.presentation.VkConversationUi -import com.meloda.fast.base.BaseFragment -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.ext.asUiText -import com.meloda.fast.ext.listenValue -import com.meloda.fast.ext.showDialog -import com.meloda.fast.ext.string -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.settings.SettingsFragment -import com.meloda.fast.ui.AppTheme -import org.koin.androidx.viewmodel.ext.android.viewModel - -class ConversationsFragment : BaseFragment(R.layout.fragment_conversations) { - - private val viewModel: ConversationsViewModel by viewModel() - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - listenViewModel() - - (view as? ComposeView)?.setContent { - ConversationsScreen() - } - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun ConversationsScreen() { - val conversations by viewModel.conversationsList.collectAsStateWithLifecycle() - val isLoading by viewModel.isLoading.collectAsStateWithLifecycle() - - val useLargeTopAppBar = AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_USE_LARGE_TOP_APP_BAR, - SettingsFragment.DEFAULT_VALUE_USE_LARGE_TOP_APP_BAR - ) - val useMultiline = AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_APPEARANCE_MULTILINE, - SettingsFragment.DEFAULT_VALUE_MULTILINE - ) - val topAppBarState = rememberTopAppBarState() - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(topAppBarState) - - val scaffoldModifier = if (useLargeTopAppBar) { - Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - } else { - Modifier.fillMaxSize() - } - - val lazyListState = rememberLazyListState() - - AppTheme { - Scaffold( - modifier = scaffoldModifier, - topBar = { - var dropDownMenuExpanded by remember { - mutableStateOf(false) - } - - val actions: @Composable RowScope.() -> Unit = @Composable { - IconButton( - onClick = { - dropDownMenuExpanded = true - } - ) { - Icon( - imageVector = Icons.Outlined.MoreVert, - contentDescription = "Options" - ) - } - - DropdownMenu( - expanded = dropDownMenuExpanded, - onDismissRequest = { - dropDownMenuExpanded = false - }, - offset = DpOffset(x = 0.dp, y = (-60).dp) - ) { - DropdownMenuItem( - onClick = { - viewModel.onToolbarMenuItemClicked(R.id.settings) - dropDownMenuExpanded = false - }, - text = { - Text(text = "Settings") - } - ) - DropdownMenuItem( - onClick = { - viewModel.onRefresh() - dropDownMenuExpanded = false - }, - text = { - Text(text = "Refresh") - } - ) - } - } - - val title = @Composable { - Text(text = if (isLoading) "Loading..." else "Conversations") - } - - if (useLargeTopAppBar) { - LargeTopAppBar( - title = title, - scrollBehavior = scrollBehavior, - actions = actions - ) - } else { - TopAppBar( - title = title, - actions = actions - ) - } - }, - floatingActionButton = { - if (!isLoading || conversations.isNotEmpty()) { - AnimatedVisibility( - visible = !lazyListState.isScrollInProgress || conversations.isNotEmpty(), - enter = slideIn(initialOffset = { IntOffset(x = 0, y = 300) }), - exit = slideOut(targetOffset = { IntOffset(x = 0, y = 300) }) - ) { - FloatingActionButton( - onClick = { - view?.performHapticFeedback(HapticFeedbackConstants.REJECT) - } - ) { - Icon( - painter = painterResource(id = R.drawable.ic_baseline_create_24), - contentDescription = null - ) - } - } - } - } - ) { padding -> - if (isLoading && conversations.isEmpty()) { - Loader() - } else { - ConversationsList( - conversations = conversations, - padding = padding, - state = lazyListState, - useMultiline = useMultiline, - ) - } - } - } - } - - @Composable - fun Loader() { - AppTheme { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } - } - - @Composable - fun ConversationsList( - conversations: List, - padding: PaddingValues, - state: LazyListState, - useMultiline: Boolean, - ) { - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .padding(padding), - state = state - ) { - items( - count = conversations.size, - key = { index -> - val item = conversations[index] - item.conversationId - } - ) { index -> - Conversation( - onItemClick = viewModel::onConversationItemClick, - onItemLongClick = viewModel::onConversationItemLongClick, - conversation = conversations[index], - maxLines = if (useMultiline) 2 else 1, - ) - - if (index < conversations.size - 1) { - Spacer(modifier = Modifier.height(8.dp)) - } - } - } - } - - // TODO: 05.08.2023, Danil Nikolaev: remove and use compose dialogs - private fun listenViewModel() = with(viewModel) { - isNeedToShowOptionsDialog.listenValue(::showOptionsDialog) - isNeedToShowDeleteDialog.listenValue(::showDeleteConversationDialog) - isNeedToShowPinDialog.listenValue(::showPinConversationDialog) - } - - // TODO: 06.04.2023, Danil Nikolaev: extract creating options to VM - private fun showOptionsDialog(conversation: VkConversationUi?) { - if (conversation == null) return - - var canPinOneMoreDialog = true - if (viewModel.conversationsList.value.size > 4) { - if (viewModel.pinnedConversationsCount.value == 5 && !conversation.isPinned) { - canPinOneMoreDialog = false - } - } - - val read = "Mark as read" - - val pin = string( - if (conversation.isPinned) R.string.conversation_context_action_unpin - else R.string.conversation_context_action_pin - ) - - val delete = string(R.string.conversation_context_action_delete) - - val params = mutableListOf>() - - conversation.lastMessage?.run { - if (!conversation.isUnread && !this.isOut) { - params += "read" to read - } - - if (!this.text.isNullOrBlank()) { - params += "share" to "Share" - } - } - - if (canPinOneMoreDialog) params += "pin" to pin - - params += "delete" to delete - - context?.showDialog( - items = params.map { param -> param.second.asUiText() }, - itemsClickAction = { index, _ -> - val key = params[index].first - viewModel.onOptionsDialogOptionClicked(conversation, key) - }, - onDismissAction = viewModel::onOptionsDialogDismissed - ) - } - - private fun showDeleteConversationDialog(conversationId: Int?) { - if (conversationId == null) return - - context?.showDialog( - title = UiText.Resource(R.string.confirm_delete_conversation), - positiveText = UiText.Resource(R.string.action_delete), - positiveAction = { viewModel.onDeleteDialogPositiveClick(conversationId) }, - negativeText = UiText.Resource(R.string.cancel), - onDismissAction = viewModel::onDeleteDialogDismissed - ) - } - - private fun showPinConversationDialog(conversation: VkConversationUi?) { - if (conversation == null) return - - context?.showDialog( - title = UiText.Resource( - if (conversation.isPinned) R.string.confirm_unpin_conversation - else R.string.confirm_pin_conversation - ), - positiveText = UiText.Resource( - if (conversation.isPinned) R.string.action_unpin - else R.string.action_pin - ), - positiveAction = { - viewModel.onPinDialogPositiveClick(conversation) - }, - negativeText = UiText.Resource(R.string.cancel), - onDismissAction = viewModel::onPinDialogDismissed - ) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsResourceProvider.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsResourceProvider.kt deleted file mode 100644 index 5c618fff..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsResourceProvider.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.screens.conversations - -import android.content.Context -import com.meloda.fast.R -import com.meloda.fast.base.ResourceProvider - -class ConversationsResourceProvider(context: Context) : ResourceProvider(context) { - - val colorPrimary = getColor(R.color.colorPrimary) - val colorOutline = getColor(R.color.colorOutline) - val colorOnPrimary = getColor(R.color.colorOnPrimary) - val colorUserAvatarAction = getColor(R.color.colorUserAvatarAction) - val colorOnUserAvatarAction = getColor(R.color.colorOnUserAvatarAction) - val colorBackground = getColor(R.color.colorBackground) - val colorBackgroundVariant = getColor(R.color.colorBackgroundVariant) - - val icLauncherColor = getColor(R.color.a1_500) - - val youPrefix = getString(R.string.you_message_prefix) - - val conversationUnreadBackground get() = getDrawable(R.drawable.ic_message_unread) - - val iconForwardedMessages = getDrawable(R.drawable.ic_attachment_forwarded_messages) - val iconForwardedMessage = getDrawable(R.drawable.ic_attachment_forwarded_message) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt deleted file mode 100644 index 04604649..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt +++ /dev/null @@ -1,573 +0,0 @@ -package com.meloda.fast.screens.conversations - -import androidx.lifecycle.viewModelScope -import coil.ImageLoader -import coil.request.ImageRequest -import com.github.terrakok.cicerone.Router -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.VKConstants -import com.meloda.fast.api.VkUtils -import com.meloda.fast.api.longpoll.LongPollEvent -import com.meloda.fast.api.longpoll.LongPollUpdatesParser -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.model.base.BaseVkGroup -import com.meloda.fast.api.model.base.BaseVkUser -import com.meloda.fast.api.model.data.BaseVkConversation -import com.meloda.fast.api.model.domain.VkConversationDomain -import com.meloda.fast.api.model.presentation.VkConversationUi -import com.meloda.fast.api.network.conversations.ConversationsDeleteRequest -import com.meloda.fast.api.network.conversations.ConversationsGetRequest -import com.meloda.fast.api.network.conversations.ConversationsPinRequest -import com.meloda.fast.api.network.conversations.ConversationsUnpinRequest -import com.meloda.fast.api.network.users.UsersGetRequest -import com.meloda.fast.base.viewmodel.BaseViewModel -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.common.Screens -import com.meloda.fast.data.conversations.ConversationsRepository -import com.meloda.fast.data.messages.MessagesRepository -import com.meloda.fast.data.users.UsersRepository -import com.meloda.fast.ext.emitOnMainScope -import com.meloda.fast.ext.emitWithMain -import com.meloda.fast.ext.findIndex -import com.meloda.fast.ext.toMap -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -// TODO: 04.08.2023, Danil Nikolaev: calculate time here and give ui ready to be shown date - -interface ConversationsViewModel { - - val pinnedConversationsCount: StateFlow - - val conversationsList: StateFlow> - val isLoading: StateFlow - - val isNeedToShowOptionsDialog: StateFlow - val isNeedToShowDeleteDialog: StateFlow - val isNeedToShowPinDialog: StateFlow - - val profiles: StateFlow> - val groups: StateFlow> - - fun onOptionsDialogDismissed() - - fun onOptionsDialogOptionClicked(conversation: VkConversationUi, key: String): Boolean - - fun onDeleteDialogDismissed() - - fun onDeleteDialogPositiveClick(conversationId: Int) - - fun onRefresh() - - fun onConversationItemClick(conversationUi: VkConversationUi) - fun onConversationItemLongClick(conversationUi: VkConversationUi): Boolean - - fun onPinDialogDismissed() - fun onPinDialogPositiveClick(conversation: VkConversationUi) - fun onToolbarMenuItemClicked(itemId: Int): Boolean -} - -class ConversationsViewModelImpl constructor( - private val conversationsRepository: ConversationsRepository, - private val usersRepository: UsersRepository, - updatesParser: LongPollUpdatesParser, - private val router: Router, - private val messagesRepository: MessagesRepository, -) : ConversationsViewModel, BaseViewModel() { - - private val dataConversations: MutableStateFlow> = - MutableStateFlow(emptyList()) - - private val domainConversations: MutableStateFlow> = - MutableStateFlow(emptyList()) - - override val conversationsList: StateFlow> = - domainConversations.map { list -> - list.map(VkConversationDomain::mapToPresentation) - }.stateIn(viewModelScope, SharingStarted.Lazily, initialValue = emptyList()) - - override val isLoading = MutableStateFlow(false) - - override val isNeedToShowOptionsDialog = MutableStateFlow(null) - override val isNeedToShowDeleteDialog = MutableStateFlow(null) - override val isNeedToShowPinDialog = MutableStateFlow(null) - - override val profiles: MutableStateFlow> = MutableStateFlow(hashMapOf()) - override val groups: MutableStateFlow> = MutableStateFlow(hashMapOf()) - - override val pinnedConversationsCount = domainConversations.map { conversations -> - val pinnedConversations = conversations.filter { it.isPinned() } - pinnedConversations.size - }.stateIn(viewModelScope, SharingStarted.Eagerly, 0) - - private val imageLoader by lazy { - ImageLoader.Builder(AppGlobal.Instance) - .crossfade(true) - .build() - } - - override fun onOptionsDialogDismissed() { - isNeedToShowOptionsDialog.emitOnMainScope(null) - } - - override fun onOptionsDialogOptionClicked( - conversation: VkConversationUi, - key: String - ): Boolean { - return when (key) { - "read" -> { - readConversation( - conversationId = conversation.conversationId, - startMessageId = conversation.lastMessageId - ) - true - } - - "delete" -> { - isNeedToShowDeleteDialog.emitOnMainScope(conversation.id) - true - } - - "pin" -> { - isNeedToShowPinDialog.emitOnMainScope(conversation) - true - } - - else -> false - } - } - - override fun onDeleteDialogDismissed() { - isNeedToShowDeleteDialog.emitOnMainScope(null) - } - - override fun onDeleteDialogPositiveClick(conversationId: Int) { - deleteConversation(conversationId) - } - - override fun onRefresh() { - loadConversations() - } - - override fun onConversationItemClick(conversationUi: VkConversationUi) { - openMessagesHistoryScreen( - conversationUi, - conversationUi.conversationUser, - conversationUi.conversationGroup - ) - } - - override fun onConversationItemLongClick(conversationUi: VkConversationUi): Boolean { - val domainConversation = conversationsList.value.find { it.id == conversationUi.id } - isNeedToShowOptionsDialog.emitOnMainScope(domainConversation) - return true - } - - override fun onPinDialogDismissed() { - isNeedToShowPinDialog.emitOnMainScope(null) - } - - override fun onPinDialogPositiveClick(conversation: VkConversationUi) { - pinConversation(conversation.id, !conversation.isPinned) - } - - override fun onToolbarMenuItemClicked(itemId: Int): Boolean { - return when (itemId) { - R.id.settings -> { - router.navigateTo(Screens.Settings()) - true - } - - else -> false - } - } - - init { - updatesParser.onNewMessage(::handleNewMessage) - updatesParser.onMessageEdited(::handleEditedMessage) - updatesParser.onMessageIncomingRead(::handleReadIncomingMessage) - updatesParser.onMessageOutgoingRead(::handleReadOutgoingMessage) - updatesParser.onConversationPinStateChanged(::handlePinStateChanged) - - loadProfileUser() - loadConversations() - } - - private fun loadConversations(offset: Int? = null) { - viewModelScope.launch(Dispatchers.IO) { - isLoading.emitWithMain(true) - sendRequest( - onError = { - isLoading.emitWithMain(false) - false - } - ) { - conversationsRepository.get( - ConversationsGetRequest( - count = 30, - extended = true, - offset = offset, - fields = VKConstants.ALL_FIELDS - ) - ) - }?.response?.let { response -> - val dataConversationsMessages = response.items.map { item -> - item.conversation to item.lastMessage - } - val dataConversationsList = dataConversationsMessages.map { pair -> pair.first } - dataConversations.emit(dataConversationsList) - - val newProfiles = response.profiles - ?.map(BaseVkUser::mapToDomain) - ?.toMap(hashMapOf(), VkUser::id) ?: hashMapOf() - profiles.emit(newProfiles) - - val newGroups = response.groups - ?.map(BaseVkGroup::mapToDomain) - ?.toMap(hashMapOf(), VkGroup::id) ?: hashMapOf() - groups.emit(newGroups) - - val messages = dataConversationsMessages - .map { pair -> pair.second } - .mapNotNull { message -> message?.asVkMessage() } - .map { message -> - message.apply { - message.user = newProfiles[message.fromId] - message.group = newGroups[message.fromId] - message.actionUser = newProfiles[message.actionMemberId] - message.actionGroup = newGroups[message.actionMemberId] - } - } - - messagesRepository.store(messages) - - val photos = newProfiles.mapNotNull { profile -> profile.value.photo200 } + - newGroups.mapNotNull { group -> group.value.photo200 } - - photos.forEach { url -> - ImageRequest.Builder(AppGlobal.Instance) - .data(url) - .build() - .let(imageLoader::enqueue) - } - - val domainConversationsList = - dataConversationsList.mapToDomain().map { conversation -> - conversation.apply { - conversation.conversationUser = newProfiles[conversation.id] - conversation.conversationGroup = newGroups[conversation.id] - } - - } - emitConversations(domainConversationsList) - - isLoading.emitWithMain(false) - } - } - } - - private suspend fun emitConversations(conversations: List) = - withContext(Dispatchers.Default) { - domainConversations.emit(conversations) - } - - private suspend fun List.mapToDomain(): List = - this.map { baseConversation -> getFilledDomainVkConversation(baseConversation) } - - private suspend fun VkConversationDomain.fill(): VkConversationDomain { - val conversation = this - val messages = messagesRepository.getCached(conversation.id) - - val lastMessage = messages.find { it.id == conversation.lastMessageId } - - val userGroup = - VkUtils.getConversationUserGroup( - conversation, - profiles.value, - groups.value - ) - val actionUserGroup = - VkUtils.getMessageActionUserGroup( - lastMessage, - profiles.value, - groups.value - ) - val messageUserGroup = - VkUtils.getMessageUserGroup( - lastMessage, - profiles.value, - groups.value - ) - - conversation.conversationUser = userGroup.first - conversation.conversationGroup = userGroup.second - - val newMessage = lastMessage?.copy()?.apply { - this.user = messageUserGroup.first - this.group = messageUserGroup.second - this.actionUser = actionUserGroup.first - this.actionGroup = actionUserGroup.second - } - - conversation.lastMessage = newMessage - - return conversation - } - - private suspend fun getFilledDomainVkConversation( - baseConversation: BaseVkConversation, - defDomainConversation: VkConversationDomain? = null, - ): VkConversationDomain { - val conversation = defDomainConversation ?: baseConversation.mapToDomain() - return conversation.fill() - } - - private fun loadProfileUser() { - viewModelScope.launch(Dispatchers.IO) { - sendRequest { - usersRepository.getById(UsersGetRequest(fields = VKConstants.USER_FIELDS)) - }?.response?.let { response -> - val users = response.map(BaseVkUser::mapToDomain) - usersRepository.storeUsers(users) - - UserConfig.vkUser.emit(users.first()) - } - } - } - - private fun deleteConversation(peerId: Int) { - viewModelScope.launch(Dispatchers.IO) { - sendRequest { - conversationsRepository.delete(ConversationsDeleteRequest(peerId)) - }?.let { - domainConversations.value.toMutableList().let { list -> - val index = list.indexOfFirst { conversation -> conversation.id == peerId } - if (index != -1) { - list.removeAt(index) - domainConversations.emit(list) - } - } - } - } - } - - private fun pinConversation(peerId: Int, pin: Boolean) { - viewModelScope.launch(Dispatchers.IO) { - if (pin) { - sendRequest { - conversationsRepository.pin(ConversationsPinRequest(peerId)) - }?.let { handleConversationPinStateUpdate(peerId, true) } - } else { - sendRequest { - conversationsRepository.unpin(ConversationsUnpinRequest(peerId)) - }?.let { - handleConversationPinStateUpdate(peerId, false) - } - } - } - } - - // TODO: 07.01.2023, Danil Nikolaev: handle major AND minor id - private suspend fun handleConversationPinStateUpdate(peerId: Int, pin: Boolean) { - withContext(Dispatchers.IO) { - val conversationsList = domainConversations.value.toMutableList() - val conversationIndex = - conversationsList.findIndex { it.id == peerId } ?: return@withContext - - val conversation = conversationsList[conversationIndex].copy( - majorId = if (pin) (pinnedConversationsCount.value + 1) * 16 - else 0 - ).fill() - - conversationsList.removeAt(conversationIndex) - - if (pin) { - conversationsList.add(0, conversation) - } else { - conversationsList.add(pinnedConversationsCount.value - 1, conversation) - } - - emitConversations(conversationsList) - } - } - - private fun handleNewMessage(event: LongPollEvent.VkMessageNewEvent) { - viewModelScope.launch(Dispatchers.IO) { - val message = event.message - - messagesRepository.store(message) - - val newProfiles: HashMap = - (profiles.value + event.profiles) as HashMap - profiles.update { newProfiles } - - val newGroups: HashMap = - (groups.value + event.groups) as HashMap - groups.update { newGroups } - - val dataConversationsList = domainConversations.value.toMutableList() - val dataConversationIndex = dataConversationsList.findIndex { it.id == message.peerId } - - if (dataConversationIndex == null) { // диалога нет в списке - // pizdets - } else { - val dataConversation = dataConversationsList[dataConversationIndex] - var newConversation = dataConversation.copy( - lastMessageId = message.id, - lastConversationMessageId = -1 - ).fill().also { - it.lastMessage = message - } - if (!message.isOut) { - newConversation = newConversation.copy( - unreadCount = newConversation.unreadCount + 1 - ).fill().also { - it.lastMessage = message - } - } - - if (dataConversation.isPinned()) { - dataConversationsList[dataConversationIndex] = newConversation - emitConversations(dataConversationsList) - return@launch - } - - dataConversationsList.removeAt(dataConversationIndex) - - val toPosition = pinnedConversationsCount.value - dataConversationsList.add(toPosition, newConversation) - - emitConversations(dataConversationsList) - } - } - } - - private fun handleEditedMessage(event: LongPollEvent.VkMessageEditEvent) { - viewModelScope.launch(Dispatchers.IO) { - val message = event.message - - messagesRepository.store(message) - - val conversationsList = domainConversations.value.toMutableList() - - val conversationIndex = conversationsList.findIndex { it.id == message.peerId } - if (conversationIndex == null) { // диалога нет в списке - - } else { - val conversation = conversationsList[conversationIndex] - conversationsList[conversationIndex] = conversation.copy( - lastMessageId = message.id, - lastConversationMessageId = -1 - ).fill().also { - it.lastMessage = message - } - - emitConversations(conversationsList) - } - } - } - - private fun handleReadIncomingMessage(event: LongPollEvent.VkMessageReadIncomingEvent) { - viewModelScope.launch(Dispatchers.IO) { - val conversationsList = domainConversations.value.toMutableList() - - val conversationIndex = - conversationsList.findIndex { it.id == event.peerId } ?: return@launch - - var conversation = conversationsList[conversationIndex] - conversation = conversation.copy( - inRead = event.messageId, - unreadCount = event.unreadCount - ).fill() - - conversationsList[conversationIndex] = conversation - - emitConversations(conversationsList) - } - } - - private fun handleReadOutgoingMessage(event: LongPollEvent.VkMessageReadOutgoingEvent) = - viewModelScope.launch(Dispatchers.IO) { - val conversationsList = domainConversations.value.toMutableList() - - val conversationIndex = - conversationsList.findIndex { it.id == event.peerId } ?: return@launch - - var conversation = conversationsList[conversationIndex] - conversation = conversation.copy( - outRead = event.messageId, - unreadCount = event.unreadCount - ).fill() - - conversationsList[conversationIndex] = conversation - - emitConversations(conversationsList) - } - - // TODO: 07.01.2023, Danil Nikolaev: handle major AND minor id - private fun handlePinStateChanged(event: LongPollEvent.VkConversationPinStateChangedEvent) = - viewModelScope.launch(Dispatchers.IO) { - val conversationsList = domainConversations.value.toMutableList() - - val conversationIndex = - conversationsList.findIndex { it.id == event.peerId } ?: return@launch - - val pin = event.majorId > 0 - - var conversation = conversationsList[conversationIndex] - conversation = conversation.copy( - majorId = event.majorId - ).fill() - - conversationsList.removeAt(conversationIndex) - - if (pin) { - conversationsList.add(0, conversation) - } else { - conversationsList.add(pinnedConversationsCount.value - 1, conversation) - } - - emitConversations(conversationsList) - } - - - private fun openMessagesHistoryScreen( - conversationUi: VkConversationUi, - user: VkUser?, - group: VkGroup?, - ) { - val conversation = domainConversations.value.find { domainConversation -> - domainConversation.id == conversationUi.id - } ?: return - - router.navigateTo(Screens.MessagesHistory(conversation, user, group)) - } - - private fun readConversation(conversationId: Int, startMessageId: Int) { - viewModelScope.launch(Dispatchers.IO) { - sendRequest { - messagesRepository.markAsRead( - peerId = conversationId, - startMessageId = startMessageId - ) - }?.response?.let { messageId -> - domainConversations.value.toMutableList().let { list -> - val index = list.findIndex { it.id == conversationId } ?: return@launch - val newConversation = list[index].copy(inRead = messageId) - list[index] = newConversation - - domainConversations.emit(list) - } - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/adapter/ConversationsDelegates.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/adapter/ConversationsDelegates.kt deleted file mode 100644 index 793eb36b..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/adapter/ConversationsDelegates.kt +++ /dev/null @@ -1,172 +0,0 @@ -package com.meloda.fast.screens.conversations.adapter - -import android.graphics.drawable.Drawable -import android.text.Spanned -import android.text.style.ForegroundColorSpan -import coil.ImageLoader -import coil.request.ImageRequest -import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.VkUtils -import com.meloda.fast.api.model.ActionState -import com.meloda.fast.api.model.presentation.VkConversationUi -import com.meloda.fast.base.adapter.OnItemClickListener -import com.meloda.fast.base.adapter.OnItemLongClickListener -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.databinding.ItemConversationBinding -import com.meloda.fast.ext.gone -import com.meloda.fast.ext.isFalse -import com.meloda.fast.ext.isTrue -import com.meloda.fast.ext.toggleVisibility -import com.meloda.fast.ext.visible -import com.meloda.fast.model.base.AdapterDiffItem -import com.meloda.fast.model.base.UiImage -import com.meloda.fast.model.base.parseString -import com.meloda.fast.model.base.setImage -import com.meloda.fast.screens.conversations.ConversationsResourceProvider -import com.meloda.fast.screens.settings.SettingsFragment - -fun conversationDelegate( - onItemClickListener: OnItemClickListener, - onItemLongClickListener: OnItemLongClickListener, -) = adapterDelegateViewBinding( - viewBinding = { layoutInflater, parent -> - ItemConversationBinding.inflate(layoutInflater, parent, false) - } -) { - binding.root.setOnClickListener { onItemClickListener.onItemClick(item) } - binding.root.setOnLongClickListener { onItemLongClickListener.onLongItemClick(item) } - - val imageLoader = ImageLoader.Builder(context) - .crossfade(true) - .build() - - val resourceProvider = ConversationsResourceProvider(context) - - bind { - val isMultilineEnabled = - AppGlobal.preferences.getBoolean(SettingsFragment.KEY_APPEARANCE_MULTILINE, true) - val maxLines = if (isMultilineEnabled) 2 else 1 - - binding.title.maxLines = maxLines - binding.message.maxLines = maxLines - - binding.container.background = - if (item.isUnread) resourceProvider.conversationUnreadBackground else null - - binding.title.text = item.title.parseString(context) - - binding.date.text = item.date - - binding.service.toggleVisibility(item.actionState != ActionState.None) - binding.phantomIcon.toggleVisibility(item.actionState == ActionState.Phantom) - binding.callIcon.toggleVisibility(item.actionState == ActionState.CallInProgress) - - binding.counter.toggleVisibility(item.unreadCount != null) - binding.counter.text = item.unreadCount - - binding.textAttachment.toggleVisibility(item.attachmentImage != null) - - binding.pin.toggleVisibility(item.isPinned) - - binding.online.toggleVisibility(item.isOnline) - - binding.avatarPlaceholder.visible() - - (item.avatar as? UiImage.Url)?.let { image -> - ImageRequest.Builder(context) - .data(image.url) - .target( - onSuccess = { result -> - binding.avatar.setImageDrawable(result) - binding.avatarPlaceholder.gone() - } - ) - .build().let(imageLoader::enqueue) - } ?: { - binding.avatar.setImage(item.avatar) { - asCircle = true - crossFade = true - onLoadedAction = { binding.avatarPlaceholder.gone() } - } - } - - val actionMessage = VkUtils.getActionConversationText( - context = context, - message = item.lastMessage, - youPrefix = resourceProvider.youPrefix, - messageUser = item.lastMessage?.user, - messageGroup = item.lastMessage?.group, - action = item.lastMessage?.getPreparedAction(), - actionUser = item.lastMessage?.actionUser, - actionGroup = item.lastMessage?.actionGroup - ) - - val attachmentIcon: Drawable? = when { - item.lastMessage?.text == null -> null - !item.lastMessage?.forwards.isNullOrEmpty() -> { - if (item.lastMessage?.forwards?.size == 1) { - resourceProvider.iconForwardedMessage - } else { - resourceProvider.iconForwardedMessages - } - } - else -> VkUtils.getAttachmentConversationIcon(context, item.lastMessage) - } - - binding.textAttachment.toggleVisibility(attachmentIcon != null) - binding.textAttachment.setImageDrawable(attachmentIcon) - - val attachmentText = (if (attachmentIcon == null) VkUtils.getAttachmentText( - message = item.lastMessage - ) else null)?.parseString(context) - - val forwardsMessage = (if (item.lastMessage?.text == null) VkUtils.getForwardsText( - message = item.lastMessage - ) else null)?.parseString(context) - - val messageText = (if ( - actionMessage != null || - forwardsMessage != null || - attachmentText != null - ) "" - else item.lastMessage?.text ?: "").run { VkUtils.prepareMessageText(this) } - - val coloredMessage = actionMessage ?: attachmentText ?: forwardsMessage ?: "" - - var prefix = when { - actionMessage != null -> "" - item.lastMessage?.isOut.isTrue -> "${resourceProvider.youPrefix}: " - else -> - when { - item.lastMessage?.isUser().isTrue && item.lastMessage?.user != null && item.lastMessage?.user?.firstName?.isNotBlank().isTrue -> { - "${item.lastMessage?.user?.firstName}: " - } - item.lastMessage?.isGroup().isTrue && item.lastMessage?.group != null && item.lastMessage?.group?.name?.isNotBlank().isTrue -> { - "${item.lastMessage?.group?.name}: " - } - else -> "" - } - } - - if ((!item.peerType.isChat() && item.lastMessage?.isOut.isFalse) || item.conversationId == UserConfig.userId) - prefix = "" - - val spanText = "$prefix$coloredMessage$messageText" - - val visualizedMessageText = VkUtils.visualizeMentions( - messageText = spanText, - resourceProvider.colorPrimary - ) - - val length = prefix.length + coloredMessage.length - visualizedMessageText.setSpan( - ForegroundColorSpan(resourceProvider.colorOutline), - 0, - length, - if (length > 0) Spanned.SPAN_EXCLUSIVE_EXCLUSIVE else 0 - ) - - binding.message.text = visualizedMessageText - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/di/ConversationsModule.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/di/ConversationsModule.kt deleted file mode 100644 index aebacc6c..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/di/ConversationsModule.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.screens.conversations.di - -import com.meloda.fast.screens.conversations.ConversationsViewModelImpl -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.dsl.module - -val conversationsModule = module { - viewModelOf(::ConversationsViewModelImpl) -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt deleted file mode 100644 index 5e74ac3e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt +++ /dev/null @@ -1,419 +0,0 @@ -package com.meloda.fast.screens.login - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.OnBackPressedCallback -import androidx.compose.animation.* -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.input.* -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.meloda.fast.R -import com.meloda.fast.base.BaseFragment -import com.meloda.fast.base.viewmodel.ViewModelUtils -import com.meloda.fast.base.viewmodel.VkEvent -import com.meloda.fast.databinding.DialogFastLoginBinding -import com.meloda.fast.ext.* -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.login.model.LoginScreenState -import com.meloda.fast.ui.AppTheme -import com.meloda.fast.ui.widgets.TextFieldErrorText -import org.koin.androidx.viewmodel.ext.android.viewModel - - -class LoginFragment : BaseFragment() { - - private val viewModel: LoginViewModel by viewModel() - - private val backPressedCallback = object : OnBackPressedCallback(false) { - override fun handleOnBackPressed() { - viewModel.onBackPressed() - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - activity?.onBackPressedDispatcher?.addCallback(backPressedCallback) - - viewModel.isNeedToShowLogo.listenValue { needToShow -> - backPressedCallback.isEnabled = !needToShow - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - listenViewModel() - - (view as? ComposeView)?.apply { - setContent { - val showLogo by viewModel.isNeedToShowLogo.collectAsState() - - AppTheme { - Surface( - color = MaterialTheme.colorScheme.background, - modifier = Modifier - .fillMaxSize() - .statusBarsPadding() - .navigationBarsPadding() - ) { - if (showLogo) { - LoginLogo() - } else { - val state by viewModel.screenState.collectAsStateWithLifecycle() - - LoginSignIn( - onSignInClick = viewModel::onSignInButtonClicked, - onLoginInputChanged = viewModel::onLoginInputChanged, - onPasswordInputChanged = viewModel::onPasswordInputChanged, - onPasswordVisibilityButtonClicked = viewModel::onPasswordVisibilityButtonClicked, - state = state, - ) - } - } - } - } - } - } - - private fun listenViewModel() = with(viewModel) { - events.listenValue(::handleEvent) - isNeedToShowErrorDialog.listenValue(::handleErrorAlertShow) - isNeedToShowFastLoginDialog.listenValue(::handleFastLoginAlertShow) - } - - private fun handleEvent(event: VkEvent) { - ViewModelUtils.parseEvent(this, event) - } - - private fun handleErrorAlertShow(isNeedToShow: Boolean) { - if (isNeedToShow) { - showErrorDialog() - } - } - - private fun handleFastLoginAlertShow(isNeedToShow: Boolean) { - if (isNeedToShow) { - showFastLoginDialog() - } - } - - private fun showErrorDialog() { - context?.showDialog( - title = UiText.Resource(R.string.title_error), - message = UiText.Simple(viewModel.screenState.value.error.orEmpty()), - positiveText = UiText.Resource(R.string.ok), - onDismissAction = viewModel::onErrorDialogDismissed - ) - } - - private fun showFastLoginDialog() { - val dialogFastLoginBinding = DialogFastLoginBinding.inflate(layoutInflater, null, false) - - context?.showDialog( - title = UiText.Resource(R.string.fast_login_title), - view = dialogFastLoginBinding.root, - positiveText = UiText.Resource(R.string.ok), - positiveAction = { - val text = dialogFastLoginBinding.fastLoginText.trimmedText - if (text.isEmpty()) return@showDialog - - val split = text.split(";") - try { - val login = split[0] - val password = split[1] - - viewModel.onLoginInputChanged(login) - viewModel.onPasswordInputChanged(password) - - viewModel.onFastLoginDialogOkButtonClicked() - } catch (e: Exception) { - e.printStackTrace() - } - }, - negativeText = UiText.Resource(R.string.cancel), - onDismissAction = viewModel::onFastLoginDialogDismissed - ) - } - - @OptIn(ExperimentalFoundationApi::class) - @Composable - fun LoginLogo() { - Box( - modifier = Modifier - .fillMaxSize() - .padding(30.dp) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image( - painter = painterResource(id = R.drawable.ic_logo_big), - contentDescription = null, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary), - modifier = Modifier.combinedClickableSound( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onLongClick = viewModel::onLogoLongClicked - ) - ) - Spacer(modifier = Modifier.height(46.dp)) - Text( - text = "Fast Messenger", - style = MaterialTheme.typography.displayMedium, - color = MaterialTheme.colorScheme.onBackground - ) - } - - FloatingActionButton( - onClick = viewModel::onLogoNextButtonClicked, - containerColor = MaterialTheme.colorScheme.secondaryContainer, - modifier = Modifier.align(Alignment.BottomCenter) - ) { - Icon( - painter = painterResource(id = R.drawable.ic_arrow_end), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSecondaryContainer - ) - } - } - } - - @Preview - @Composable - fun LoginSignInPreview() { - AppTheme( - useDarkTheme = false, - useDynamicColors = false - ) { - Surface(color = MaterialTheme.colorScheme.background) { - LoginSignIn( - state = LoginScreenState.EMPTY, - onSignInClick = { }, - onLoginInputChanged = {}, - onPasswordInputChanged = {}, - onPasswordVisibilityButtonClicked = {} - ) - } - } - } - - @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) - @Composable - fun LoginSignIn( - onSignInClick: () -> Unit, - onLoginInputChanged: (String) -> Unit, - onPasswordInputChanged: (String) -> Unit, - onPasswordVisibilityButtonClicked: () -> Unit, - state: LoginScreenState - ) { - val focusManager = LocalFocusManager.current - val (loginFocusable, passwordFocusable) = FocusRequester.createRefs() - val isLoading = state.isLoading - - val goButtonClickAction = { - if (!isLoading) { - focusManager.clearFocus() - onSignInClick.invoke() - } - } - val loginFieldTabClick = { - passwordFocusable.requestFocus() - true - } - - Box( - modifier = Modifier - .fillMaxSize() - .padding(30.dp) - .imePadding() - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.Center) - ) { - Text( - text = "Sign in to VK", - color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.displayMedium - ) - - Spacer(modifier = Modifier.height(58.dp)) - - var loginText by remember { mutableStateOf(TextFieldValue(state.login)) } - val showLoginError = state.loginError - - TextField( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)) - .handleEnterKey(loginFieldTabClick::invoke) - .handleTabKey(loginFieldTabClick::invoke) - .focusRequester(loginFocusable), - value = loginText, - onValueChange = { newText -> - loginText = newText - onLoginInputChanged.invoke(newText.text) - }, - label = { Text(text = "Login") }, - placeholder = { Text(text = "Login") }, - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.ic_round_person_24), - contentDescription = null, - tint = if (showLoginError) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.primary - } - ) - }, - shape = RoundedCornerShape(10.dp), - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Email - ), - keyboardActions = KeyboardActions(onNext = { passwordFocusable.requestFocus() }), - isError = showLoginError, - singleLine = true - ) - AnimatedVisibility(visible = showLoginError) { - TextFieldErrorText(text = "Field must not be empty") - } - - Spacer(modifier = Modifier.height(16.dp)) - - var passwordText by remember { mutableStateOf(TextFieldValue(state.password)) } - val showPasswordError = state.passwordError - var passwordVisible = state.passwordVisible - - TextField( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)) - .handleEnterKey { - goButtonClickAction.invoke() - true - } - .focusRequester(passwordFocusable), - value = passwordText, - onValueChange = { newText -> - passwordText = newText - onPasswordInputChanged.invoke(newText.text) - }, - label = { Text(text = "Password") }, - placeholder = { Text(text = "Password") }, - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.round_vpn_key_24), - contentDescription = null, - tint = if (showPasswordError) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.primary - } - ) - }, - trailingIcon = { - val imagePainter = painterResource( - id = if (passwordVisible) R.drawable.round_visibility_off_24 - else R.drawable.round_visibility_24 - ) - - IconButton( - onClick = { - onPasswordVisibilityButtonClicked.invoke() - passwordVisible = !passwordVisible - } - ) { - Icon(painter = imagePainter, contentDescription = null) - } - }, - shape = RoundedCornerShape(10.dp), - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Go, - keyboardType = KeyboardType.Password - ), - keyboardActions = KeyboardActions( - onGo = { goButtonClickAction.invoke() } - ), - isError = showPasswordError, - visualTransformation = if (passwordVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, - singleLine = true - ) - AnimatedVisibility(visible = showPasswordError) { - TextFieldErrorText(text = "Field must not be empty") - } - } - - Box( - modifier = Modifier.align(Alignment.BottomCenter), - contentAlignment = Alignment.Center - ) { - - FloatingActionButton( - onClick = goButtonClickAction::invoke, - containerColor = MaterialTheme.colorScheme.secondaryContainer - ) { - Icon( - painter = painterResource(id = R.drawable.ic_arrow_end), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSecondaryContainer - ) - } - AnimatedVisibility( - visible = isLoading, - enter = fadeIn(), - exit = fadeOut() - ) { - CircularProgressIndicator() - } - } - } - } - - companion object { - - fun newInstance(): LoginFragment { - return LoginFragment() - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginScreens.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginScreens.kt deleted file mode 100644 index 7b379af1..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginScreens.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.meloda.fast.screens.login - -import com.github.terrakok.cicerone.androidx.FragmentScreen - -object LoginScreens { - - fun login() = FragmentScreen { - LoginFragment.newInstance() - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt deleted file mode 100644 index 5527d5a9..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt +++ /dev/null @@ -1,370 +0,0 @@ -package com.meloda.fast.screens.login - -import androidx.lifecycle.viewModelScope -import com.github.terrakok.cicerone.Router -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.VKConstants -import com.meloda.fast.api.network.WrongTwoFaCodeError -import com.meloda.fast.api.network.WrongTwoFaCodeFormatError -import com.meloda.fast.api.network.auth.AuthDirectRequest -import com.meloda.fast.base.viewmodel.CaptchaRequiredEvent -import com.meloda.fast.base.viewmodel.DeprecatedBaseViewModel -import com.meloda.fast.base.viewmodel.UnknownErrorEvent -import com.meloda.fast.base.viewmodel.ValidationRequiredEvent -import com.meloda.fast.base.viewmodel.VkEvent -import com.meloda.fast.common.Screens -import com.meloda.fast.data.account.AccountsDao -import com.meloda.fast.data.auth.AuthRepository -import com.meloda.fast.ext.emitOnMainScope -import com.meloda.fast.ext.emitOnScope -import com.meloda.fast.ext.listenValue -import com.meloda.fast.ext.updateValue -import com.meloda.fast.model.AppAccount -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.captcha.screen.CaptchaArguments -import com.meloda.fast.screens.captcha.screen.CaptchaResult -import com.meloda.fast.screens.captcha.screen.CaptchaScreen -import com.meloda.fast.screens.login.model.LoginScreenState -import com.meloda.fast.screens.login.model.LoginValidationResult -import com.meloda.fast.screens.login.validation.LoginValidator -import com.meloda.fast.screens.twofa.model.TwoFaArguments -import com.meloda.fast.screens.twofa.model.TwoFaResult -import com.meloda.fast.screens.twofa.model.TwoFaValidationType -import com.meloda.fast.screens.twofa.screen.TwoFaScreen -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch - -interface LoginViewModel { - val events: Flow - - val isNeedToShowLogo: StateFlow - - val screenState: StateFlow - - val isNeedToShowFastLoginDialog: Flow - val isNeedToShowErrorDialog: Flow - - fun onBackPressed() - - fun onPasswordVisibilityButtonClicked() - - fun onLogoNextButtonClicked() - - fun onLoginInputChanged(newLogin: String) - fun onPasswordInputChanged(newPassword: String) - - fun onSignInButtonClicked() - fun onSignInButtonLongClicked() - - fun onFastLoginDialogOkButtonClicked() - - fun onFastLoginDialogDismissed() - fun onErrorDialogDismissed() - fun onLogoLongClicked() -} - -class LoginViewModelImpl constructor( - private val authRepository: AuthRepository, - private val router: Router, - private val accounts: AccountsDao, - private val loginValidator: LoginValidator, - private val captchaScreen: CaptchaScreen, - private val twoFaScreen: TwoFaScreen -) : DeprecatedBaseViewModel(), LoginViewModel { - - override val isNeedToShowLogo = MutableStateFlow(true) - - override val screenState = MutableStateFlow(LoginScreenState.EMPTY) - - private val validationState: StateFlow> = - screenState.map(loginValidator::validate) - .stateIn(viewModelScope, SharingStarted.Eagerly, listOf(LoginValidationResult.Empty)) - - private val captchaResult = captchaScreen.resultFlow - private val twoFaResult = twoFaScreen.resultFlow - - override val events = MutableSharedFlow( - extraBufferCapacity = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - override val isNeedToShowErrorDialog = MutableStateFlow(false) - override val isNeedToShowFastLoginDialog = MutableStateFlow(false) - - private var currentValidationEvent: ValidationRequiredEvent? = null - - init { - tasksEvent.listenValue(::handleEvent) - - captchaResult.listenValue { result -> - when (result) { - is CaptchaResult.Success -> { - val sid = result.sid - val code = result.code - val newState = screenState.value.copy( - captchaSid = sid, captchaCode = code - ) - screenState.updateValue(newState) - - login() - } - - else -> Unit - } - } - - twoFaResult.listenValue { result -> - when (result) { - is TwoFaResult.Success -> { - val sid = result.sid - val code = result.code - val newState = screenState.value.copy( - validationSid = sid, validationCode = code - ) - screenState.updateValue(newState) - - login() - } - - else -> Unit - } - } - } - - private fun handleEvent(event: VkEvent) { - when (event) { - is CaptchaRequiredEvent -> onCaptchaEventReceived(event) - is ValidationRequiredEvent -> onValidationEventReceived(event) - else -> events.emitOnScope(event) - } - } - - override fun onBackPressed() { - if (isNeedToShowLogo.value) { - router.exit() - } else { - isNeedToShowLogo.updateValue(true) - } - } - - override fun onPasswordVisibilityButtonClicked() { - val newState = screenState.value.copy( - passwordVisible = !screenState.value.passwordVisible - ) - screenState.updateValue(newState) - } - - override fun onLogoNextButtonClicked() { - isNeedToShowLogo.emitOnMainScope(false) - } - - override fun onLoginInputChanged(newLogin: String) { - val newState = screenState.value.copy( - login = newLogin.trim(), - loginError = false - ) - screenState.updateValue(newState) - } - - override fun onPasswordInputChanged(newPassword: String) { - val newState = screenState.value.copy( - password = newPassword.trim(), - passwordError = false - ) - screenState.updateValue(newState) - } - - private fun onCaptchaEventReceived(event: CaptchaRequiredEvent) { - val captchaSid = event.sid - val captchaImage = event.image - - val newState = screenState.value.copy( - captchaSid = captchaSid, - captchaImage = captchaImage - ) - screenState.update { newState } - - showCaptchaScreen( - CaptchaArguments( - captchaSid = captchaSid, - captchaImage = captchaImage - ) - ) - } - - private fun showCaptchaScreen(args: CaptchaArguments) { - captchaScreen.show(router, args) - } - - private fun onValidationEventReceived(event: ValidationRequiredEvent) { - currentValidationEvent = event - - val validationSid = event.sid - val newForm = screenState.value.copy( - validationSid = validationSid - ) - screenState.update { newForm } - - showValidationScreen( - TwoFaArguments( - validationSid = event.sid, - redirectUri = event.redirectUri, - phoneMask = event.phoneMask, - validationType = TwoFaValidationType.parse(event.validationType), - canResendSms = event.canResendSms, - wrongCodeError = event.codeError - ) - ) - } - - private fun showValidationScreen(args: TwoFaArguments) { - twoFaScreen.show(router, args) - } - - override fun onSignInButtonClicked() { - login() - } - - override fun onSignInButtonLongClicked() { - isNeedToShowFastLoginDialog.emitOnMainScope(true) - } - - override fun onFastLoginDialogOkButtonClicked() { - login() - } - - override fun onFastLoginDialogDismissed() { - isNeedToShowFastLoginDialog.emitOnMainScope(false) - } - - override fun onErrorDialogDismissed() { - isNeedToShowErrorDialog.emitOnMainScope(false) - } - - override fun onLogoLongClicked() { - router.navigateTo(Screens.Settings()) - } - - private fun login(forceSms: Boolean = false) { - currentValidationEvent?.let { event -> - if (!screenState.value.validationSid.isNullOrBlank() && screenState.value.validationCode == null) { - handleEvent(event) - return - } - } - - val state = screenState.value.copy() - - val clearedState = screenState.value.copy( - captchaSid = null, - captchaImage = null, - captchaCode = null, - validationSid = null, - validationCode = null - ) - - screenState.update { clearedState } - - processValidation() - if (!validationState.value.contains(LoginValidationResult.Valid)) return - - viewModelScope.launch(Dispatchers.IO) { - var newState = screenState.value.copy( - isLoading = true - ) - screenState.update { newState } - - sendRequest( - onError = { error -> - when (error) { - is WrongTwoFaCodeError, WrongTwoFaCodeFormatError -> { - currentValidationEvent?.let { event -> - val codeError = UiText.Simple( - if (error is WrongTwoFaCodeError) "Wrong code" - else "Wrong code format" - ) - handleEvent(event.copy(codeError = codeError)) - true - } ?: false - } - - else -> false - } - }, - request = { - val requestModel = AuthDirectRequest( - grantType = VKConstants.Auth.GrantType.PASSWORD, - clientId = VKConstants.VK_APP_ID, - clientSecret = VKConstants.VK_SECRET, - username = state.login, - password = state.password, - scope = VKConstants.Auth.SCOPE, - twoFaForceSms = forceSms, - twoFaCode = state.validationCode, - captchaSid = state.captchaSid, - captchaKey = state.captchaCode - ) - - authRepository.auth(requestModel) - } - )?.let { response -> - val userId = response.userId - val accessToken = response.accessToken - - if (userId == null || accessToken == null) { - sendEvent(UnknownErrorEvent) - return@let - } - - if (currentValidationEvent != null) { - currentValidationEvent = null - } - - val currentAccount = AppAccount( - userId = userId, - accessToken = accessToken, - fastToken = null - ).also { account -> - UserConfig.currentUserId = account.userId - UserConfig.userId = account.userId - UserConfig.accessToken = account.accessToken - UserConfig.fastToken = account.fastToken - } - - accounts.insert(listOf(currentAccount)) - - router.replaceScreen(Screens.Main()) - } - - newState = screenState.value.copy( - isLoading = false - ) - screenState.update { newState } - } - } - - private fun processValidation() { - validationState.value.forEach { result -> - when (result) { - LoginValidationResult.LoginEmpty -> { - screenState.updateValue(screenState.value.copy(loginError = true)) - } - - LoginValidationResult.PasswordEmpty -> { - screenState.updateValue(screenState.value.copy(passwordError = true)) - } - - LoginValidationResult.Empty -> Unit - LoginValidationResult.Valid -> Unit - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/di/LoginModule.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/di/LoginModule.kt deleted file mode 100644 index a7e4c4c0..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/di/LoginModule.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.screens.login.di - -import com.meloda.fast.screens.login.LoginViewModelImpl -import com.meloda.fast.screens.login.validation.LoginValidator -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.dsl.module - -val loginModule = module { - single { LoginValidator() } - viewModelOf(::LoginViewModelImpl) -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/model/LoginResult.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/model/LoginResult.kt deleted file mode 100644 index 03f3090d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/model/LoginResult.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.meloda.fast.screens.login.model - -sealed class LoginResult { - object Authorized : LoginResult() - object Cancelled : LoginResult() -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/model/LoginValidationResult.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/model/LoginValidationResult.kt deleted file mode 100644 index 923ef6b7..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/model/LoginValidationResult.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.meloda.fast.screens.login.model - -sealed class LoginValidationResult { - - object LoginEmpty : LoginValidationResult() - - object PasswordEmpty : LoginValidationResult() - - object Empty : LoginValidationResult() - - object Valid : LoginValidationResult() -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/screen/LoginScreen.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/screen/LoginScreen.kt deleted file mode 100644 index d49612d2..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/screen/LoginScreen.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.meloda.fast.screens.login.screen - -import com.github.terrakok.cicerone.Router -import com.meloda.fast.base.screen.AppScreen -import com.meloda.fast.base.screen.createResultFlow -import com.meloda.fast.screens.login.model.LoginResult - -class LoginScreen : AppScreen { - - override val resultFlow = createResultFlow() - override var args: Unit = Unit - - override fun show(router: Router, args: Unit) { - this.args = args - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt deleted file mode 100644 index bf48d0d6..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.meloda.fast.screens.main - -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.os.bundleOf -import androidx.fragment.app.setFragmentResult -import com.meloda.fast.base.BaseFragment -import com.meloda.fast.base.viewmodel.ViewModelUtils -import com.meloda.fast.base.viewmodel.VkEvent -import com.meloda.fast.ext.listenValue -import com.meloda.fast.screens.main.activity.ServicesState -import org.koin.androidx.viewmodel.ext.android.viewModel - -class MainFragment : BaseFragment() { - - private val viewModel: MainViewModel by viewModel() - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - Log.d("MainFragment", "onCreate: viewModel: $viewModel") - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = View(context) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - listenViewModel() - } - - private fun listenViewModel() { - viewModel.events.listenValue(::onEvent) - - viewModel.servicesState.listenValue { state -> - val enableServices = state == ServicesState.Started - setFragmentResult( - START_SERVICES_KEY, - bundleOf(START_SERVICES_ARG_ENABLE to enableServices) - ) - } - } - - private fun onEvent(event: VkEvent) { - ViewModelUtils.parseEvent(this, event) - } - - companion object { - const val START_SERVICES_KEY = "start_services" - const val START_SERVICES_ARG_ENABLE = "enable" - - fun newInstance(): MainFragment = MainFragment() - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/main/MainViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/main/MainViewModel.kt deleted file mode 100644 index d34e1391..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/MainViewModel.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.meloda.fast.screens.main - -import android.util.Log -import androidx.lifecycle.viewModelScope -import com.github.terrakok.cicerone.Router -import com.github.terrakok.cicerone.Screen -import com.meloda.fast.api.UserConfig -import com.meloda.fast.base.viewmodel.DeprecatedBaseViewModel -import com.meloda.fast.base.viewmodel.VkEvent -import com.meloda.fast.common.Screens -import com.meloda.fast.screens.main.activity.ServicesState -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch - -interface MainViewModel { - val events: Flow - - val servicesState: Flow -} - -class MainViewModelImpl constructor( - private val router: Router -) : MainViewModel, DeprecatedBaseViewModel() { - - override val events = tasksEvent.map { it } - - override val servicesState = MutableStateFlow(ServicesState.Unknown) - - init { - checkSession() - } - - private fun checkSession() { - viewModelScope.launch { - val currentUserId = UserConfig.currentUserId - val userId = UserConfig.userId - val accessToken = UserConfig.accessToken - val fastToken = UserConfig.fastToken - - Log.d( - "MainViewModel", - "checkSession: currentUserId: $currentUserId; userId: $userId; accessToken: $accessToken; fastToken: $fastToken" - ) - - if (UserConfig.isLoggedIn()) { - servicesState.emit(ServicesState.Started) - openScreen(Screens.Conversations()) - } else { - servicesState.emit(ServicesState.Stopped) - openScreen(Screens.Login()) - } - } - } - - private fun openScreen(screen: Screen) { - router.replaceScreen(screen) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/main/activity/LongPollState.kt b/app/src/main/kotlin/com/meloda/fast/screens/main/activity/LongPollState.kt deleted file mode 100644 index 0e0ebf16..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/activity/LongPollState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.meloda.fast.screens.main.activity - -sealed class LongPollState { - object ForegroundService : LongPollState() - object DefaultService : LongPollState() - object Stop : LongPollState() -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/main/activity/LongPollUtils.kt b/app/src/main/kotlin/com/meloda/fast/screens/main/activity/LongPollUtils.kt deleted file mode 100644 index d0078dcb..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/activity/LongPollUtils.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.meloda.fast.screens.main.activity - -import android.Manifest -import android.content.Intent -import android.net.Uri -import android.provider.Settings -import androidx.core.content.edit -import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.lifecycleScope -import com.fondesa.kpermissions.coroutines.sendSuspend -import com.fondesa.kpermissions.extension.permissionsBuilder -import com.fondesa.kpermissions.isGranted -import com.fondesa.kpermissions.isPermanentlyDenied -import com.meloda.fast.R -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.ext.sdk33AndUp -import com.meloda.fast.ext.showDialog -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.settings.SettingsFragment -import kotlinx.coroutines.launch - -object LongPollUtils { - - fun requestNotificationsPermission( - fragmentActivity: FragmentActivity, - onStateChangedAction: (LongPollState) -> Unit, - fromSettings: Boolean = false, - ) { - val longPollInForegroundEnabled = - AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, - SettingsFragment.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND - ) - - sdk33AndUp { - fragmentActivity.lifecycleScope.launch { - val result = - fragmentActivity.permissionsBuilder(Manifest.permission.POST_NOTIFICATIONS) - .build() - .sendSuspend() - .first() - - val resultToEmit: LongPollState = when { - longPollInForegroundEnabled && result.isGranted() -> LongPollState.ForegroundService - else -> LongPollState.DefaultService - } - - onStateChangedAction.invoke(resultToEmit) - - val isLongPollOnlyInsideApp = - AppGlobal.preferences.getBoolean("lp_inside_app", false) - - if (result.isGranted()) { - AppGlobal.preferences.edit { putBoolean("lp_inside_app", false) } - } - - if (longPollInForegroundEnabled && - !result.isGranted() && - (!isLongPollOnlyInsideApp || fromSettings) - ) { - showNotificationsPermissionAlert( - fragmentActivity, - onStateChangedAction, - result.isPermanentlyDenied(), - ) - } - } - } ?: run { - onStateChangedAction.invoke( - if (longPollInForegroundEnabled) LongPollState.ForegroundService - else LongPollState.DefaultService - ) - } - } - - private fun showNotificationsPermissionAlert( - fragmentActivity: FragmentActivity, - onStateChangedAction: (LongPollState) -> Unit, - permanentlyDenied: Boolean, - ) { - val positiveText = - UiText.Simple(if (permanentlyDenied) "Open settings" else "Grant") - val positiveAction = { - if (permanentlyDenied) { - val intent = Intent( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.parse("package:${fragmentActivity.packageName}") - ) - intent.addCategory(Intent.CATEGORY_DEFAULT) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - - try { - fragmentActivity.startActivity(intent) - } catch (e: Exception) { - e.printStackTrace() - } - } else { - requestNotificationsPermission(fragmentActivity, onStateChangedAction) - } - } - - val neutralText = - if (permanentlyDenied) UiText.Resource(R.string.ok) - else UiText.Simple("Dismiss") - val neutralAction = { - if (permanentlyDenied) { - AppGlobal.preferences.edit { - putBoolean("lp_inside_app", true) - putBoolean(SettingsFragment.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, false) - } - } else Unit - } - - fragmentActivity.showDialog( - title = UiText.Resource(R.string.warning), - message = UiText.Simple( - "You denied notifications permission." + - "\nWithout notifications LongPoll service will work only inside app." + - "\nThis means that messages will only be updated while app is on the screen" - ), - positiveText = positiveText, - positiveAction = positiveAction, - neutralText = neutralText, - neutralAction = neutralAction, - isCancelable = false - ) - } - -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/main/activity/MainActivity.kt b/app/src/main/kotlin/com/meloda/fast/screens/main/activity/MainActivity.kt deleted file mode 100644 index 2fcda73b..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/activity/MainActivity.kt +++ /dev/null @@ -1,316 +0,0 @@ -package com.meloda.fast.screens.main.activity - -import android.app.NotificationChannel -import android.app.NotificationManager -import android.content.Context -import android.content.Intent -import android.os.Build -import android.os.Bundle -import android.util.Log -import android.widget.Toast -import androidx.core.content.ContextCompat -import androidx.core.content.edit -import androidx.lifecycle.lifecycleScope -import com.github.terrakok.cicerone.NavigatorHolder -import com.github.terrakok.cicerone.Router -import com.github.terrakok.cicerone.androidx.AppNavigator -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.meloda.fast.BuildConfig -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.longpoll.LongPollUpdatesParser -import com.meloda.fast.base.BaseActivity -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.common.Screens -import com.meloda.fast.data.account.AccountsDao -import com.meloda.fast.ext.edgeToEdge -import com.meloda.fast.ext.isSdkAtLeast -import com.meloda.fast.ext.listenValue -import com.meloda.fast.screens.main.MainFragment -import com.meloda.fast.screens.main.activity.LongPollUtils.requestNotificationsPermission -import com.meloda.fast.screens.settings.SettingsFragment -import com.meloda.fast.service.LongPollService -import com.meloda.fast.service.OnlineService -import com.meloda.fast.util.AndroidUtils -import com.microsoft.appcenter.AppCenter -import com.microsoft.appcenter.analytics.Analytics -import com.microsoft.appcenter.crashes.Crashes -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch -import org.koin.android.ext.android.inject - -class MainActivity : BaseActivity(R.layout.activity_main) { - - private val navigator = object : AppNavigator(this, R.id.root_fragment_container) {} - - private val navigatorHolder: NavigatorHolder by inject() - - private val router: Router by inject() - - private val accountsDao: AccountsDao by inject() - - private val updatesParser: LongPollUpdatesParser by inject() - - private var isOnlineServiceWasLaunched: Boolean = false - - private var savedInstanceState: Bundle? = null - - override fun onResumeFragments() { - navigatorHolder.setNavigator(navigator) - super.onResumeFragments() - } - - override fun onPause() { - if (isOnlineServiceWasLaunched) { - toggleOnlineService(false) - } - navigatorHolder.removeNavigator() - super.onPause() - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - this.savedInstanceState = savedInstanceState - edgeToEdge() - - createNotificationChannels() - - AppCenter.configure(application, BuildConfig.msAppCenterAppToken) - - if (!BuildConfig.DEBUG) { - AppCenter.start(Analytics::class.java) - } - - val enableCrashLogs = - AppGlobal.preferences.getBoolean(SettingsFragment.KEY_MS_APPCENTER_ENABLE, true) - || (BuildConfig.DEBUG && AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_MS_APPCENTER_ENABLE_ON_DEBUG, - false - )) - - if (enableCrashLogs) { - AppCenter.start(Crashes::class.java) - } - - if (UserConfig.currentUserId == -1) { - openMainScreen() - } else { - initUserConfig() - } - - // TODO: 09.04.2023, Danil Nikolaev: implement checking updates on startup - - // TODO: 09.04.2023, Danil Nikolaev: rewrite this - supportFragmentManager.setFragmentResultListener( - MainFragment.START_SERVICES_KEY, - this - ) { _, result -> - val enable = result.getBoolean(MainFragment.START_SERVICES_ARG_ENABLE, true) - if (enable) { - requestNotificationsPermission( - fragmentActivity = this, - onStateChangedAction = { state -> - lifecycleScope.launch { longPollState.emit(state) } - } - ) - - startServices() - } else { - stopServices() - } - } - - // TODO: 09.04.2023, Danil Nikolaev: rewrite this - longPollState.listenValue { state -> - stopLongPollService() - - when (state) { - LongPollState.DefaultService -> startLongPollService(false) - LongPollState.ForegroundService -> startLongPollService(true) - else -> Unit - } - } - } - - private fun createNotificationChannels() { - isSdkAtLeast(Build.VERSION_CODES.O) { - val dialogsName = "Dialogs" - val dialogsDescriptionText = "Channel for dialogs notifications" - val dialogsImportance = NotificationManager.IMPORTANCE_HIGH - val dialogsChannel = - NotificationChannel("simple_notifications", dialogsName, dialogsImportance).apply { - description = dialogsDescriptionText - } - - val longPollName = "Long Polling" - val longPollDescriptionText = "Channel for long polling service" - val longPollImportance = NotificationManager.IMPORTANCE_NONE - val longPollChannel = - NotificationChannel("long_polling", longPollName, longPollImportance).apply { - description = longPollDescriptionText - } - - val notificationManager: NotificationManager = - getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - notificationManager.createNotificationChannels(listOf(dialogsChannel, longPollChannel)) - } - } - - override fun onResume() { - super.onResume() - - if (isOnlineServiceWasLaunched) { - toggleOnlineService(true) - } - - Crashes.getLastSessionCrashReport().thenAccept { report -> - if (report != null) { - if (AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_DEBUG_SHOW_CRASH_ALERT, - true - ) - ) { - val stackTrace = report.stackTrace - - MaterialAlertDialogBuilder(this) - .setTitle(R.string.app_crash_occurred) - .setMessage("Stacktrace: $stackTrace") - .setPositiveButton(R.string.ok, null) - .setNegativeButton(R.string.copy) { _, _ -> - AndroidUtils.copyText( - label = "Fast_Crash_Report", - text = stackTrace - ) - Toast.makeText(this, "Copied", Toast.LENGTH_SHORT).show() - } - .setNeutralButton(R.string.share) { _, _ -> - val sendIntent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, stackTrace) - type = "text/plain" - } - - val shareIntent = Intent.createChooser(sendIntent, "Share stacktrace") - try { - startActivity(shareIntent) - } catch (e: Exception) { - e.printStackTrace() - - runOnUiThread { - MaterialAlertDialogBuilder(this) - .setTitle(R.string.warning) - .setMessage("Can't share") - .setPositiveButton(R.string.ok, null) - .show() - } - } - } - .show() - } - } - } - - if (AppGlobal.preferences.getBoolean(LongPollService.KeyLongPollWasDestroyed, false)) { - AppGlobal.preferences.edit { - putBoolean(LongPollService.KeyLongPollWasDestroyed, false) - } - - startServices() - } - } - - private fun startServices() { - toggleOnlineService(true) - } - - private fun stopServices() { - toggleOnlineService(false) - } - - private fun createLongPollIntent(asForeground: Boolean? = null): Intent = - Intent(this, LongPollService::class.java).apply { - asForeground?.let { putExtra("foreground", it) } - } - - private fun startLongPollService(asForeground: Boolean) { - val longPollIntent = createLongPollIntent(asForeground) - - if (asForeground) { - ContextCompat.startForegroundService(this, longPollIntent) - } else { - startService(longPollIntent) - } - } - - private fun stopLongPollService() { - stopService(createLongPollIntent()) - } - - private fun toggleOnlineService(enable: Boolean) { - if (enable) { - isOnlineServiceWasLaunched = true - startService(Intent(this, OnlineService::class.java)) - } else { - stopService(Intent(this, OnlineService::class.java)) - } - } - - private fun initUserConfig() { - if (UserConfig.currentUserId == -1) return - - lifecycleScope.launch { - val accounts = accountsDao.getAll() - - Log.d("MainActivity", "initUserConfig: accounts: $accounts") - if (accounts.isNotEmpty()) { - val currentAccount = accounts.find { it.userId == UserConfig.currentUserId } - if (currentAccount != null) { - UserConfig.parse(currentAccount) - } - - openMainScreen() - } else { - openMainScreen() - } - } - } - - private fun openMainScreen() { - if (savedInstanceState != null) return - - var needToOpenSettings = false - - if (intent.dataString != null) { - intent.dataString?.let { data -> - if (data == "shortcut_settings") { - needToOpenSettings = true - } - } - } - - if (intent.hasExtra("data")) { - if (intent.getStringExtra("data") == "open_settings") { - needToOpenSettings = true - } - } - - if (needToOpenSettings) { - router.newRootScreen(Screens.Settings()) - } else { - router.newRootScreen(Screens.Main()) - } - } - - override fun onDestroy() { - super.onDestroy() - stopServices() - updatesParser.clearListeners() - isOnlineServiceWasLaunched = false - savedInstanceState = null - } - - companion object { - val longPollState = MutableStateFlow(LongPollState.Stop) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/main/activity/ServicesState.kt b/app/src/main/kotlin/com/meloda/fast/screens/main/activity/ServicesState.kt deleted file mode 100644 index 8a16a8ae..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/activity/ServicesState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.meloda.fast.screens.main.activity - -sealed class ServicesState { - object Started : ServicesState() - object Stopped : ServicesState() - object Unknown : ServicesState() -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/main/di/MainModule.kt b/app/src/main/kotlin/com/meloda/fast/screens/main/di/MainModule.kt deleted file mode 100644 index 1305bdb7..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/di/MainModule.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.screens.main.di - -import com.meloda.fast.screens.main.MainViewModelImpl -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.dsl.module - -val mainModule = module { - viewModelOf(::MainViewModelImpl) -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt deleted file mode 100644 index 52b55b52..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt +++ /dev/null @@ -1,628 +0,0 @@ -package com.meloda.fast.screens.messages - -import android.content.Context -import android.content.res.Resources -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.Space -import androidx.appcompat.widget.AppCompatTextView -import androidx.appcompat.widget.LinearLayoutCompat -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.content.ContextCompat -import androidx.core.view.isNotEmpty -import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams -import androidx.core.view.updateMarginsRelative -import androidx.core.view.updatePadding -import com.bumptech.glide.Priority -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.VkUtils -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.model.attachments.VkAttachment -import com.meloda.fast.api.model.attachments.VkAudio -import com.meloda.fast.api.model.attachments.VkCall -import com.meloda.fast.api.model.attachments.VkFile -import com.meloda.fast.api.model.attachments.VkGift -import com.meloda.fast.api.model.attachments.VkGraffiti -import com.meloda.fast.api.model.attachments.VkLink -import com.meloda.fast.api.model.attachments.VkPhoto -import com.meloda.fast.api.model.attachments.VkSticker -import com.meloda.fast.api.model.attachments.VkStory -import com.meloda.fast.api.model.attachments.VkVideo -import com.meloda.fast.api.model.attachments.VkVoiceMessage -import com.meloda.fast.api.model.attachments.VkWall -import com.meloda.fast.api.model.base.BaseVkMessage -import com.meloda.fast.databinding.ItemMessageAttachmentAudioBinding -import com.meloda.fast.databinding.ItemMessageAttachmentCallBinding -import com.meloda.fast.databinding.ItemMessageAttachmentFileBinding -import com.meloda.fast.databinding.ItemMessageAttachmentForwardsBinding -import com.meloda.fast.databinding.ItemMessageAttachmentGeoBinding -import com.meloda.fast.databinding.ItemMessageAttachmentGiftBinding -import com.meloda.fast.databinding.ItemMessageAttachmentGraffitiBinding -import com.meloda.fast.databinding.ItemMessageAttachmentLinkBinding -import com.meloda.fast.databinding.ItemMessageAttachmentPhotoBinding -import com.meloda.fast.databinding.ItemMessageAttachmentReplyBinding -import com.meloda.fast.databinding.ItemMessageAttachmentStickerBinding -import com.meloda.fast.databinding.ItemMessageAttachmentStoryBinding -import com.meloda.fast.databinding.ItemMessageAttachmentVideoBinding -import com.meloda.fast.databinding.ItemMessageAttachmentVoiceBinding -import com.meloda.fast.databinding.ItemMessageAttachmentWallPostBinding -import com.meloda.fast.ext.ImageLoader.clear -import com.meloda.fast.ext.ImageLoader.loadWithGlide -import com.meloda.fast.ext.TypeTransformations -import com.meloda.fast.ext.dpToPx -import com.meloda.fast.ext.gone -import com.meloda.fast.ext.orDots -import com.meloda.fast.ext.toggleVisibility -import com.meloda.fast.ext.toggleVisibilityIfHasContent -import com.meloda.fast.ext.visible -import com.meloda.fast.model.base.parseString -import com.meloda.fast.util.AndroidUtils -import java.text.SimpleDateFormat -import java.util.Locale -import kotlin.math.roundToInt - -class AttachmentInflater constructor( - private val context: Context, - private val container: LinearLayoutCompat, - private val replyContainer: FrameLayout, - private val timeReadContainer: View, - private val message: VkMessage, - private val profiles: Map, - private val groups: Map, -) { - private lateinit var attachments: List - - private val inflater = LayoutInflater.from(context) - - private val colorPrimary = ContextCompat.getColor( - context, - R.color.colorPrimary - ) - private val colorBackground = ContextCompat.getColor( - context, - R.color.colorBackground - ) - private val colorSecondary = ContextCompat.getColor( - context, - R.color.colorSecondary - ) - - private val timeReadBackground = ContextCompat.getDrawable( - context, - R.drawable.time_read_indicator_on_attachments_background - ) - - private var photoClickListener: ((url: String) -> Unit)? = null - private var replyClickListener: ((replyMessage: VkMessage) -> Unit)? = null - private var forwardsClickListener: ((forwards: List) -> Unit)? = null - - private val displayMetrics get() = Resources.getSystem().displayMetrics - - fun withPhotoClickListener(block: ((url: String) -> Unit)?): AttachmentInflater { - this.photoClickListener = block - return this - } - - fun withReplyClickListener(block: ((replyMessage: VkMessage) -> Unit)?): AttachmentInflater { - this.replyClickListener = block - return this - } - - fun withForwardsClickListener(block: ((forwards: List) -> Unit)?): AttachmentInflater { - this.forwardsClickListener = block - return this - } - - fun inflate() { - container.removeAllViews() - replyContainer.removeAllViews() - - replyContainer.toggleVisibility(message.hasReply()) - container.toggleVisibility( - !message.attachments.isNullOrEmpty() - || message.hasForwards() - || message.hasGeo() - ) - - timeReadContainer.run { - updateLayoutParams { - val margin = (if (container.isVisible) 6 else 2).dpToPx() - updateMarginsRelative(end = margin, bottom = margin) - } - - background = if (container.isVisible) timeReadBackground else null - } - - if (message.hasReply()) { - reply(requireNotNull(message.replyMessage)) - } - - if (message.hasForwards()) { - forwards(requireNotNull(message.forwards)) - } - - if (message.hasGeo()) { - geo(requireNotNull(message.geo)) - } - - if (message.attachments.isNullOrEmpty()) return - attachments = requireNotNull(message.attachments) - - if (attachments.size == 1) { - when (val attachment = attachments[0]) { - is VkSticker -> return sticker(attachment) - is VkWall -> return wall(attachment) - is VkVoiceMessage -> return voice(attachment) - is VkCall -> return call(attachment) - is VkGraffiti -> return graffiti(attachment) - is VkGift -> return gift(attachment) - is VkStory -> return story(attachment) - } - } - - if (attachments.size > 1) { - if (VkUtils.isAttachmentsHaveOneType(attachments) && attachments[0] is VkPhoto) { - return attachments.forEach { photo(it as VkPhoto) } - } - - if (VkUtils.isAttachmentsHaveOneType(attachments) && attachments[0] is VkVideo) { - return attachments.forEach { video(it as VkVideo) } - } - - if (VkUtils.isAttachmentsHaveOneType(attachments) && attachments[0] is VkAudio) { - return attachments.forEach { audio(it as VkAudio) } - } - - if (VkUtils.isAttachmentsHaveOneType(attachments) && attachments[0] is VkFile) { - return attachments.forEach { file(it as VkFile) } - } - } - - attachments.forEach { attachment -> - when (attachment) { - is VkPhoto -> photo(attachment) - is VkVideo -> video(attachment) - is VkAudio -> audio(attachment) - is VkFile -> file(attachment) - is VkLink -> link(attachment) - - else -> unknown(attachment) - } - } - } - - private fun unknown(attachment: VkAttachment) { - val attachmentType = attachment.javaClass.name - Log.e( - "Attachment inflater", - "Unknown attachment type: $attachmentType" - ) - - val textView = AppCompatTextView(context) - textView.text = attachmentType - - container.addView(textView) - } - - private fun reply(replyMessage: VkMessage) { - val binding = ItemMessageAttachmentReplyBinding.inflate(inflater, replyContainer, true) - binding.root.setOnClickListener { replyClickListener?.invoke(replyMessage) } - - val attachmentText = (VkUtils.getAttachmentText( - message = replyMessage - ))?.parseString(context) - - val forwardsMessage = (if (replyMessage.text == null) VkUtils.getForwardsText( - message = replyMessage - ) else null)?.parseString(context) - - val messageText = attachmentText ?: forwardsMessage ?: (replyMessage.text.orDots()).run { - VkUtils.prepareMessageText(this) - } - - binding.text.text = VkUtils.visualizeMentions( - messageText = messageText, - mentionColor = colorPrimary - ) - - val replyUserGroup = VkUtils.getMessageUserGroup(replyMessage, profiles, groups) - - val fromUser: VkUser? = replyUserGroup.first - val fromGroup: VkGroup? = replyUserGroup.second - - val title = VkUtils.getMessageTitle(replyMessage, fromUser, fromGroup) - binding.title.text = title.orDots() - } - - private fun forwards(forwards: List) { - val binding = ItemMessageAttachmentForwardsBinding.inflate(inflater, container, true) - - binding.root.setOnClickListener { forwardsClickListener?.invoke(forwards) } - } - - private fun geo(geo: BaseVkMessage.Geo) { - val binding = ItemMessageAttachmentGeoBinding.inflate(inflater, container, true) - - binding.location.text = geo.place.title - binding.location.toggleVisibilityIfHasContent() - } - - private fun photo(photo: VkPhoto) { - val size = photo.getSizeOrSmaller(VkPhoto.SIZE_TYPE_807) ?: return - - val specRatio = size.width.toFloat() / size.height.toFloat() - val widthMultiplier: Float = when { - specRatio > 1 -> 0.7F - specRatio < 1 -> 0.45F - else -> 0.35F - } - val ratio = "${size.width}:${size.height}" - - val spacer = Space(context).apply { - layoutParams = - LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 5.dpToPx()) - } - - if (container.isNotEmpty()) { - container.addView(spacer) - } - - val binding = ItemMessageAttachmentPhotoBinding.inflate(inflater, container, true) - - val cornersRadius = 17.dpToPx().toFloat() - - binding.border.run { - shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius) - - updateLayoutParams { - width = (displayMetrics.widthPixels * widthMultiplier).roundToInt() - dimensionRatio = ratio - } - loadWithGlide { - imageDrawable = ColorDrawable(colorSecondary) - loadPriority = Priority.IMMEDIATE - cacheStrategy = DiskCacheStrategy.NONE - } - } - - binding.image.run { - shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius * 0.8F) - - setOnClickListener { - photo.getMaxSize()?.let { size -> photoClickListener?.invoke(size.url) } - } - - loadWithGlide { - imageUrl = size.url - crossFade = true - placeholderDrawable = ColorDrawable(colorBackground) - loadPriority = Priority.LOW - } - } - } - - private fun video(video: VkVideo) { - val spacer = Space(context).apply { - layoutParams = - LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 5.dpToPx()) - } - if (container.isNotEmpty()) { - container.addView(spacer) - } - - val size = video.imageForWidthAtLeast(300) ?: return - val binding = ItemMessageAttachmentVideoBinding.inflate(inflater, container, true) - - val specRatio = size.width.toFloat() / size.height.toFloat() - val widthMultiplier: Float = when { - specRatio > 1 -> 0.7F - specRatio < 1 -> 0.45F - else -> 0.35F - } - val ratio = "${size.width}:${size.height}" - - val cornersRadius = 17.dpToPx().toFloat() - - binding.border.run { - shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius) - - updateLayoutParams { - width = (displayMetrics.widthPixels * widthMultiplier).roundToInt() - dimensionRatio = ratio - } - loadWithGlide { - imageDrawable = ColorDrawable(colorSecondary) - loadPriority = Priority.IMMEDIATE - cacheStrategy = DiskCacheStrategy.NONE - } - } - - binding.image.run { - shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius * 0.8F) - - loadWithGlide { - imageUrl = size.url - crossFade = true - placeholderDrawable = ColorDrawable(colorBackground) - loadPriority = Priority.LOW - } - } - } - - private fun audio(audio: VkAudio) { - val binding = ItemMessageAttachmentAudioBinding.inflate(inflater, container, true) - - binding.title.text = audio.title - binding.artist.text = "%s | %s".format( - audio.artist, - SimpleDateFormat("mm:ss", Locale.getDefault()).format(audio.duration * 1000L) - ) - } - - private fun file(file: VkFile) { - val binding = ItemMessageAttachmentFileBinding.inflate(inflater, container, true) - - binding.title.text = file.title - binding.size.text = "%s | %s".format( - AndroidUtils.bytesToHumanReadableSize(file.size.toDouble()), - file.ext.uppercase() - ) - } - - private fun link(link: VkLink) { - val binding = ItemMessageAttachmentLinkBinding.inflate( - inflater, container, true - ) - - binding.title.text = link.title - binding.title.toggleVisibility(!link.title.isNullOrBlank()) - - binding.caption.text = link.caption - binding.caption.toggleVisibility(!link.caption.isNullOrBlank()) - - link.photo?.getSizeOrSmaller('y')?.let { size -> - binding.preview.loadWithGlide { - imageUrl = size.url - crossFade = true - } - binding.linkIcon.gone() - return - } - - binding.preview.setImageDrawable( - ColorDrawable( - ContextCompat.getColor( - context, - R.color.a3_200 - ) - ) - ) - binding.linkIcon.visible() - } - - private fun sticker(sticker: VkSticker) { - val binding = ItemMessageAttachmentStickerBinding.inflate(inflater, container, true) - - val url = sticker.urlForSize(352) - - binding.image.run { - val size = 140.dpToPx() - - layoutParams = LinearLayoutCompat.LayoutParams(size, size) - - loadWithGlide { - imageUrl = url - crossFade = true - } - } - } - - private fun wall(wall: VkWall) { - val binding = ItemMessageAttachmentWallPostBinding.inflate(inflater, container, true) - - val group = if (wall.fromId > 0) null else groups[wall.fromId] - val user = if (wall.fromId < 0) null else profiles[wall.fromId] - - val postTitleRes = when { - group != null && user == null -> R.string.post_type_community - user != null && group == null -> R.string.post_type_user - else -> R.string.post_type_unknown - } - - val avatar = when { - group == null && user != null -> user.photo200 - user == null && group != null -> group.photo200 - else -> null - } - - val title = (when { - group == null && user != null -> user.fullName - user == null && group != null -> group.name - else -> null - }).orDots() - - binding.postTitle.text = context.getString(postTitleRes) - binding.postTitle.gone() - - binding.avatar.toggleVisibility(group != null || user != null) - - if (binding.avatar.isVisible) { - binding.avatar.loadWithGlide { - imageUrl = avatar - crossFade = true - } - } else { - binding.avatar.clear() - } - - binding.title.text = title - - binding.date.text = SimpleDateFormat( - "dd.MM.yyyy HH:mm", - Locale.getDefault() - ).format(wall.date * 1000L) - } - - private fun voice(voiceMessage: VkVoiceMessage) { - val binding = ItemMessageAttachmentVoiceBinding.inflate(inflater, container, true) - - if (message.isOut) { - val padding = 6.dpToPx() - binding.root.updatePadding( - bottom = padding, - left = padding - ) - } - val waveform = IntArray(voiceMessage.waveform.size) - voiceMessage.waveform.forEachIndexed { index, i -> waveform[index] = i } - - binding.waveform.sample = waveform - binding.waveform.maxProgress = 100f - binding.waveform.progress = 100f - - binding.duration.text = SimpleDateFormat( - "mm:ss", - Locale.getDefault() - ).format(voiceMessage.duration * 1000L) - } - - private fun call(call: VkCall) { - val binding = ItemMessageAttachmentCallBinding.inflate(inflater, container, true) - - if (message.isOut) - binding.root.updatePadding( - bottom = 5.dpToPx(), - left = 6.dpToPx() - ) - - val callType = - context.getString( - if (call.initiatorId == UserConfig.userId) R.string.message_call_type_outgoing - else R.string.message_call_type_incoming - ) - - binding.type.text = callType - - var callState = - context.getString( - if (call.state == "reached") R.string.message_call_state_ended - else if (call.state == "canceled_by_initiator") { - if (call.initiatorId == UserConfig.userId) R.string.message_call_state_cancelled - else R.string.message_call_state_missed - } else R.string.message_call_unknown - ) - - if (callState == context.getString(R.string.message_call_unknown)) callState = call.state - - binding.state.text = callState - } - - private fun graffiti(graffiti: VkGraffiti) { - val binding = ItemMessageAttachmentGraffitiBinding.inflate(inflater, container, true) - - val url = graffiti.url - - val size = 140.dpToPx() - - val heightCoefficient = graffiti.height / size.toFloat() - - binding.image.run { - layoutParams = LinearLayoutCompat.LayoutParams( - size, - (graffiti.height / heightCoefficient).roundToInt() - ) - - loadWithGlide { - imageUrl = url - crossFade = true - } - } - } - - private fun gift(gift: VkGift) { - val binding = ItemMessageAttachmentGiftBinding.inflate(inflater, container, true) - - val url = gift.thumb256 ?: gift.thumb96 ?: gift.thumb48 - - binding.image.run { - val size = 140.dpToPx() - - shapeAppearanceModel = shapeAppearanceModel.withCornerSize(12.dpToPx().toFloat()) - - layoutParams = LinearLayoutCompat.LayoutParams(size, size) - - loadWithGlide { - imageUrl = url - crossFade = true - } - } - } - - private fun story(story: VkStory) { - val binding = ItemMessageAttachmentStoryBinding.inflate(inflater, container, true) - - val photoUrl = story.photo?.getSizeOrSmaller(VkPhoto.SIZE_TYPE_807)?.url - - val dimmerDrawable = - ContextCompat.getDrawable(context, R.drawable.ic_message_attachment_story_image_dimmer) - - val cornersRadius = 24.dpToPx() - - binding.caption.updateLayoutParams { - val margin = cornersRadius / 2 - updateMarginsRelative( - top = margin, - start = margin, - end = margin, - bottom = margin - ) - } - - binding.dimmer.loadWithGlide { - imageDrawable = dimmerDrawable - transformations = listOf(TypeTransformations.RoundedCornerCrop(cornersRadius)) - loadPriority = Priority.IMMEDIATE - cacheStrategy = DiskCacheStrategy.NONE - } - - binding.image.run { - shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius.toFloat()) - - loadWithGlide { - imageUrl = photoUrl - crossFade = true - placeholderDrawable = ColorDrawable(Color.GRAY) - } - } - - if (story.ownerId == UserConfig.userId) { - binding.caption.text = context.getString(R.string.message_attachment_story_your_story) - } else { - val storyOwnerUser = if (story.isFromUser()) profiles[story.ownerId] else null - val storyOwnerGroup = if (story.isFromGroup()) groups[story.ownerId] else null - - val ownerName = when { - storyOwnerUser != null -> storyOwnerUser.fullName - storyOwnerGroup != null -> storyOwnerGroup.name - else -> null - } - - binding.caption.text = context.getString( - R.string.message_attachment_story_story_from, - ownerName - ) - binding.caption.toggleVisibility(ownerName != null) - binding.dimmer.toggleVisibility(binding.caption.isVisible) - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentsAdapter.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentsAdapter.kt deleted file mode 100644 index 97c15eb8..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentsAdapter.kt +++ /dev/null @@ -1,237 +0,0 @@ -package com.meloda.fast.screens.messages - -import android.content.Context -import android.view.View -import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.DiffUtil -import com.google.android.material.shape.ShapeAppearanceModel -import com.meloda.fast.R -import com.meloda.fast.api.model.attachments.VkAttachment -import com.meloda.fast.api.model.attachments.VkAudio -import com.meloda.fast.api.model.attachments.VkFile -import com.meloda.fast.api.model.attachments.VkPhoto -import com.meloda.fast.api.model.attachments.VkVideo -import com.meloda.fast.base.adapter.BaseAdapter -import com.meloda.fast.base.adapter.BaseHolder -import com.meloda.fast.databinding.ItemUploadedAttachmentAudioBinding -import com.meloda.fast.databinding.ItemUploadedAttachmentFileBinding -import com.meloda.fast.databinding.ItemUploadedAttachmentPhotoBinding -import com.meloda.fast.databinding.ItemUploadedAttachmentVideoBinding -import com.meloda.fast.ext.ImageLoader.clear -import com.meloda.fast.ext.ImageLoader.loadWithGlide -import com.meloda.fast.ext.dpToPx -import com.meloda.fast.ext.gone -import com.meloda.fast.ext.toggleVisibility -import com.meloda.fast.ext.visible - -class AttachmentsAdapter( - context: Context, - preAddedValues: List, - private var onRemoveClickedListener: ((position: Int) -> Unit)? = null, -) : BaseAdapter( - context, comparator, preAddedValues -) { - - private companion object { - - private const val TypePhoto = 1 - private const val TypeVideo = 2 - private const val TypeAudio = 3 - private const val TypeFile = 4 - - private val comparator = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: VkAttachment, newItem: VkAttachment): Boolean { - return oldItem == newItem - } - - override fun areContentsTheSame(oldItem: VkAttachment, newItem: VkAttachment): Boolean { - return false - } - } - } - - private val colorPrimaryVariant = ContextCompat.getColor(context, R.color.colorPrimaryVariant) - - open inner class Holder(v: View) : BaseHolder(v) - - override fun getItemViewType(position: Int): Int { - return when (getItem(position)) { - is VkPhoto -> TypePhoto - is VkVideo -> TypeVideo - is VkAudio -> TypeAudio - is VkFile -> TypeFile - else -> super.getItemViewType(position) - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { - return when (viewType) { - TypePhoto -> PhotoViewHolder( - ItemUploadedAttachmentPhotoBinding.inflate(inflater, parent, false) - ) - TypeVideo -> VideoViewHolder( - ItemUploadedAttachmentVideoBinding.inflate(inflater, parent, false) - ) - TypeAudio -> AudioViewHolder( - ItemUploadedAttachmentAudioBinding.inflate(inflater, parent, false) - ) - TypeFile -> FileViewHolder( - ItemUploadedAttachmentFileBinding.inflate(inflater, parent, false) - ) - else -> Holder(View(context)) - } - } - - inner class PhotoViewHolder( - private val binding: ItemUploadedAttachmentPhotoBinding, - ) : Holder(binding.root) { - - init { - binding.image.shapeAppearanceModel = - binding.image.shapeAppearanceModel.withCornerSize(18.dpToPx().toFloat()) - } - - override fun bind(position: Int) { - val photo = getItem(position) as VkPhoto - - binding.progressBar.visible() - - val onDoneAction = { binding.progressBar.gone() } - - binding.image.loadWithGlide { - imageUrl = photo.getSizeOrSmaller(VkPhoto.SIZE_TYPE_807)?.url - crossFade = true - placeholderColor = colorPrimaryVariant - onLoadedAction = onDoneAction - onFailedAction = onDoneAction - } - - binding.close.setOnClickListener { - onRemoveClickedListener?.invoke(position) - } - } - } - - inner class VideoViewHolder( - private val binding: ItemUploadedAttachmentVideoBinding, - ) : Holder(binding.root) { - init { - val cornerSizedShapeAppearanceModel = ShapeAppearanceModel().withCornerSize( - 18.dpToPx().toFloat() - ) - - binding.image.shapeAppearanceModel = cornerSizedShapeAppearanceModel - binding.coloredBackground.shapeAppearanceModel = cornerSizedShapeAppearanceModel - } - - override fun bind(position: Int) { - val video = getItem(position) as VkVideo - - binding.title.text = video.title - - val previewSrc = video.imageForWidthAtLeast(300) - binding.image.toggleVisibility(previewSrc != null) - binding.coloredBackground.toggleVisibility(previewSrc == null) - binding.videoIcon.toggleVisibility(previewSrc == null) - - if (previewSrc != null) { - binding.progressBar.visible() - - binding.image.loadWithGlide { - imageUrl = previewSrc.url - crossFade = true - placeholderColor = colorPrimaryVariant - onLoadedAction = { binding.progressBar.gone() } - onFailedAction = { showPlaceholder() } - } - } else { - binding.progressBar.gone() - binding.image.clear() - } - - binding.close.setOnClickListener { - onRemoveClickedListener?.invoke(position) - } - } - - private fun showPlaceholder() { - binding.coloredBackground.visible() - binding.videoIcon.visible() - binding.image.clear() - binding.image.gone() - binding.progressBar.gone() - } - } - - inner class AudioViewHolder( - private val binding: ItemUploadedAttachmentAudioBinding, - ) : Holder(binding.root) { - init { - binding.coloredBackground.shapeAppearanceModel = - binding.coloredBackground.shapeAppearanceModel.withCornerSize(18.dpToPx().toFloat()) - } - - override fun bind(position: Int) { - val audio = getItem(position) as VkAudio - - binding.title.text = audio.title - - binding.close.setOnClickListener { - onRemoveClickedListener?.invoke(position) - } - } - } - - inner class FileViewHolder( - private val binding: ItemUploadedAttachmentFileBinding, - ) : Holder(binding.root) { - - init { - val cornerSizedShapeAppearanceModel = ShapeAppearanceModel().withCornerSize( - 18.dpToPx().toFloat() - ) - - binding.image.shapeAppearanceModel = cornerSizedShapeAppearanceModel - binding.coloredBackground.shapeAppearanceModel = cornerSizedShapeAppearanceModel - } - - override fun bind(position: Int) { - val file = getItem(position) as VkFile - - binding.title.text = file.title - - val previewSrc = file.preview?.photo?.sizes?.get(0) - binding.image.toggleVisibility(previewSrc != null) - binding.coloredBackground.toggleVisibility(previewSrc == null) - binding.fileIcon.toggleVisibility(previewSrc == null) - - if (previewSrc != null) { - binding.progressBar.visible() - - binding.image.loadWithGlide { - imageUrl = previewSrc.src - crossFade = true - placeholderColor = colorPrimaryVariant - onLoadedAction = { binding.progressBar.gone() } - onFailedAction = { showPlaceholder() } - } - } else { - binding.progressBar.gone() - binding.image.clear() - } - - binding.close.setOnClickListener { - onRemoveClickedListener?.invoke(position) - } - } - - private fun showPlaceholder() { - binding.coloredBackground.visible() - binding.fileIcon.visible() - binding.image.clear() - binding.image.gone() - binding.progressBar.gone() - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/ForwardedMessagesFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/ForwardedMessagesFragment.kt deleted file mode 100644 index 8c68224a..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/ForwardedMessagesFragment.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.meloda.fast.screens.messages - -import android.os.Bundle -import android.view.View -import androidx.core.os.bundleOf -import by.kirich1409.viewbindingdelegate.viewBinding -import com.github.terrakok.cicerone.Router -import com.meloda.fast.R -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.model.domain.VkConversationDomain -import com.meloda.fast.base.BaseFragment -import com.meloda.fast.common.Screens -import com.meloda.fast.databinding.FragmentForwardedMessagesBinding -import com.meloda.fast.ext.getParcelableArrayListCompat -import com.meloda.fast.ext.getParcelableCompat -import com.meloda.fast.ext.getSerializableCompat -import dev.chrisbanes.insetter.applyInsetter -import org.koin.android.ext.android.inject - -class ForwardedMessagesFragment : BaseFragment(R.layout.fragment_forwarded_messages) { - - private val router: Router by inject() - - private val binding by viewBinding(FragmentForwardedMessagesBinding::bind) - - private var conversation: VkConversationDomain? = null - private var messages: List = emptyList() - private var profiles = hashMapOf() - private var groups = hashMapOf() - - private val adapter: MessagesHistoryAdapter by lazy { - MessagesHistoryAdapter( - this, requireNotNull(conversation), profiles, groups - ) - } - - @Suppress("UNCHECKED_CAST") - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - requireArguments().run { - conversation = getParcelableCompat(ArgConversation, VkConversationDomain::class.java) - - messages = getParcelableArrayListCompat(ArgMessages, VkMessage::class.java) - ?: emptyList() - - profiles = - getSerializableCompat(ArgProfiles, HashMap::class.java) as? HashMap - ?: hashMapOf() - groups = - getSerializableCompat(ArgGroups, HashMap::class.java) as? HashMap - ?: hashMapOf() - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.recyclerView.applyInsetter { - type(navigationBars = true) { padding() } - } - - binding.toolbar.applyInsetter { - type(statusBars = true) { padding() } - } - binding.toolbar.setNavigationOnClickListener { requireActivity().onBackPressedDispatcher.onBackPressed() } - - fillRecyclerView() - } - - private fun fillRecyclerView() { - adapter.setItems(messages) - binding.recyclerView.adapter = adapter - } - - fun scrollToMessage(messageId: Int) { - adapter.searchMessageIndex(messageId)?.let { index -> - binding.recyclerView.scrollToPosition(index) - } - } - - fun openForwardsScreen( - conversation: VkConversationDomain, - messages: List, - profiles: HashMap = hashMapOf(), - groups: HashMap = hashMapOf() - ) { - router.navigateTo( - Screens.ForwardedMessages(conversation, messages, profiles, groups) - ) - } - - companion object { - private const val ArgConversation = "conversation" - private const val ArgMessages = "messages" - private const val ArgProfiles = "profiles" - private const val ArgGroups = "groups" - - fun newInstance( - conversation: VkConversationDomain, - messages: List, - profiles: HashMap = hashMapOf(), - groups: HashMap = hashMapOf() - ): ForwardedMessagesFragment { - val fragment = ForwardedMessagesFragment() - fragment.arguments = bundleOf( - ArgConversation to conversation, - ArgMessages to messages, - ArgProfiles to profiles, - ArgGroups to groups - ) - - return fragment - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt deleted file mode 100644 index bbbb680d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt +++ /dev/null @@ -1,377 +0,0 @@ -package com.meloda.fast.screens.messages - -import android.content.Context -import android.content.Intent -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.net.Uri -import android.util.Log -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.widget.LinearLayoutCompat -import androidx.core.util.ObjectsCompat -import androidx.core.view.isVisible -import androidx.recyclerview.widget.DiffUtil -import com.meloda.fast.R -import com.meloda.fast.api.VkUtils -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.model.attachments.VkPhoto -import com.meloda.fast.api.model.domain.VkConversationDomain -import com.meloda.fast.base.adapter.BaseAdapter -import com.meloda.fast.base.adapter.BaseHolder -import com.meloda.fast.databinding.ItemMessageInBinding -import com.meloda.fast.databinding.ItemMessageOutBinding -import com.meloda.fast.databinding.ItemMessageServiceBinding -import com.meloda.fast.ext.ImageLoader.loadWithGlide -import com.meloda.fast.ext.dpToPx - -class MessagesHistoryAdapter constructor( - context: Context, - val conversation: VkConversationDomain, - val profiles: HashMap = hashMapOf(), - val groups: HashMap = hashMapOf(), -) : BaseAdapter( - context, - Comparator -) { - - constructor( - fragment: MessagesHistoryFragment, - conversation: VkConversationDomain, - profiles: HashMap = hashMapOf(), - groups: HashMap = hashMapOf(), - ) : this(fragment.requireContext(), conversation, profiles, groups) { - this.messagesHistoryFragment = fragment - } - - constructor( - fragment: ForwardedMessagesFragment, - conversation: VkConversationDomain, - profiles: HashMap = hashMapOf(), - groups: HashMap = hashMapOf(), - ) : this(fragment.requireContext(), conversation, profiles, groups) { - this.isForwards = true - this.forwardedMessagesFragment = fragment - } - - private var isForwards: Boolean = false - - private var messagesHistoryFragment: MessagesHistoryFragment? = null - private var forwardedMessagesFragment: ForwardedMessagesFragment? = null - - var avatarLongClickListener: ((position: Int) -> Unit)? = null - - override fun getItemViewType(position: Int): Int { - return when (val item = getItem(position)) { - is VkMessage -> { - return when { - item.action != null -> TypeService - item.isOut -> TypeOutgoing - !item.isOut -> TypeIncoming - else -> -1 - } - } - else -> -1 - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BasicHolder { - return when (viewType) { - TypeService -> ServiceMessage( - ItemMessageServiceBinding.inflate(inflater, parent, false) - ) - TypeOutgoing -> OutgoingMessage( - ItemMessageOutBinding.inflate(inflater, parent, false) - ) - TypeIncoming -> IncomingMessage( - ItemMessageInBinding.inflate(inflater, parent, false) - ) - else -> throw IllegalStateException("Wrong viewType: $viewType") - } - } - - override fun onBindViewHolder(holder: BasicHolder, position: Int) { - if (holder is Header || holder is Footer) { - Log.d( - "MessagesHistoryAdapter", - "onBindViewHolder: index $position, holder: ${holder.javaClass.simpleName}. Skip" - ) - return - } - - Log.d( - "MessagesHistoryAdapter", - "onBindViewHolder: index $position, holder: ${holder.javaClass.simpleName}. Bind" - ) - - initListeners(holder.itemView, position) - holder.bind(position) - } - - open inner class BasicHolder(v: View = View(context)) : BaseHolder(v) - - inner class Header(v: View) : BasicHolder(v) - - inner class Footer(v: View) : BasicHolder(v) - - inner class IncomingMessage( - private val binding: ItemMessageInBinding, - ) : BasicHolder(binding.root) { - - override fun bind(position: Int, payloads: MutableList?) { - val message = getItem(position) as VkMessage - - val prevMessage = getOrNull(position - 1) - val nextMessage = getOrNull(position + 1) - - MessagesPreparator( - context = context, - position = position, - adapterClickListener = itemClickListener, - payloads = payloads, - - root = binding.root, - - conversation = conversation, - message = message, - prevMessage = prevMessage, - nextMessage = nextMessage, - - title = binding.title, - - avatar = binding.avatar, - bubble = binding.bubble, - text = binding.text, - spacer = binding.spacer, - messageState = binding.messageState, - time = binding.time, - - replyContainer = binding.replyContainer, - attachmentContainer = binding.attachmentContainer, - timeReadContainer = binding.timeReadContainer, - - profiles = profiles, - groups = groups, - - isForwards = isForwards - ) - .withPhotoClickListener { - Intent(Intent.ACTION_VIEW, Uri.parse(it)).run { - context.startActivity(this) - } - } - .withReplyClickListener { - messagesHistoryFragment?.scrollToMessage(it.id) - forwardedMessagesFragment?.scrollToMessage(it.id) - } - .withForwardsClickListener { messages -> - messagesHistoryFragment?.openForwardsScreen( - conversation, messages, profiles, groups - ) - forwardedMessagesFragment?.openForwardsScreen( - conversation, messages, profiles, groups - ) - } - .prepare() - - binding.avatar.setOnLongClickListener { - avatarLongClickListener?.invoke(position) - true - } - } - } - - inner class OutgoingMessage( - private val binding: ItemMessageOutBinding, - ) : BasicHolder(binding.root) { - - override fun bind(position: Int, payloads: MutableList?) { - val message = getItem(position) - val prevMessage = getOrNull(position - 1) - - MessagesPreparator( - context = context, - position = position, - adapterClickListener = itemClickListener, - payloads = payloads, - root = binding.root, - conversation = conversation, - message = message, - prevMessage = prevMessage, - - bubble = binding.bubble, - text = binding.text, - spacer = binding.spacer, - messageState = binding.messageState, - time = binding.time, - - timeReadContainer = binding.timeReadContainer, - replyContainer = binding.replyContainer, - attachmentContainer = binding.attachmentContainer, - - profiles = profiles, - groups = groups, - - isForwards = isForwards - ) - .withPhotoClickListener { - Intent(Intent.ACTION_VIEW, Uri.parse(it)).run { - context.startActivity(this) - } - } - .withReplyClickListener { - messagesHistoryFragment?.scrollToMessage(it.id) - forwardedMessagesFragment?.scrollToMessage(it.id) - } - .withForwardsClickListener { messages -> - messagesHistoryFragment?.openForwardsScreen( - conversation, messages, profiles, groups - ) - forwardedMessagesFragment?.openForwardsScreen( - conversation, messages, profiles, groups - ) - } - .prepare() - } - } - - inner class ServiceMessage( - private val binding: ItemMessageServiceBinding, - ) : BasicHolder(binding.root) { - - private val youPrefix = context.getString(R.string.you_message_prefix) - - init { - binding.photo.shapeAppearanceModel = - binding.photo.shapeAppearanceModel.withCornerSize(4.dpToPx().toFloat()) - } - - override fun bind(position: Int) { - val message = getItem(position) as VkMessage - - val messageUser = - if (message.isUser()) profiles[message.fromId] - else null - - val messageGroup = - if (message.isGroup()) groups[message.fromId] - else null - - message.action ?: return - - binding.message.text = VkUtils.getActionMessageText( - context = context, - message = message, - youPrefix = youPrefix, - messageUser = messageUser, - messageGroup = messageGroup, - action = message.getPreparedAction(), - actionUser = null, - actionGroup = null, - ) - - val attachments = message.attachments ?: return - attachments[0].let { attachment -> - if (attachment !is VkPhoto) return@let - - binding.photo.isVisible = true - - val size = attachment.getSizeOrSmaller('y') ?: return@let - - binding.photo.layoutParams = LinearLayoutCompat.LayoutParams( - size.width, - size.height - ) - - binding.photo.loadWithGlide { - imageUrl = size.url - crossFade = true - placeholderDrawable = ColorDrawable(Color.LTGRAY) - } - - binding.photo.setOnClickListener { - Intent(Intent.ACTION_VIEW, Uri.parse(size.url)).run { - context.startActivity(this) - } - } - } - } - } - - fun containsUnreadMessages(isOutgoingMessages: Boolean = false): Boolean { - for (i in indices) { - val item = getItem(i) - if (item !is VkMessage) continue - - if (item.isOut == isOutgoingMessages && !item.isRead(conversation)) { - return true - } - } - return false - } - - fun containsRandomId(randomId: Int): Boolean { - if (randomId == 0) return false - for (i in indices) { - val item = getItem(i) - if (item !is VkMessage) continue - - if (item.randomId == randomId) return true - } - - return false - } - - fun containsId(id: Int): Boolean { - for (i in indices) { - val item = getItem(i) - if (item !is VkMessage) continue - - if (item.id == id) return true - } - - return false - } - - fun searchMessageIndex(messageId: Int): Int? { - for (i in indices) { - val message = getItem(i) - if (message is VkMessage && message.id == messageId) return i - } - - return null - } - - fun searchMessageById(messageId: Int): VkMessage? { - for (i in indices) { - val message = getItem(i) - if (message is VkMessage && message.id == messageId) return message - } - - return null - } - - companion object { - private const val TypeService = 1 - private const val TypeIncoming = 3 - private const val TypeOutgoing = 4 - - private val Comparator = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: VkMessage, - newItem: VkMessage, - ): Boolean { - return oldItem.id == newItem.id - } - - override fun areContentsTheSame( - oldItem: VkMessage, - newItem: VkMessage, - ): Boolean { - return ObjectsCompat.equals(oldItem, newItem) && (oldItem.state == newItem.state) - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt deleted file mode 100644 index f02915f6..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt +++ /dev/null @@ -1,1531 +0,0 @@ -package com.meloda.fast.screens.messages - -import android.animation.ValueAnimator -import android.content.Context -import android.net.Uri -import android.os.Bundle -import android.os.Environment -import android.provider.OpenableColumns -import android.util.Log -import android.view.HapticFeedbackConstants -import android.view.View -import android.view.animation.LinearInterpolator -import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.widget.PopupMenu -import androidx.core.animation.doOnEnd -import androidx.core.content.ContextCompat -import androidx.core.os.bundleOf -import androidx.core.view.isVisible -import androidx.core.view.updatePaddingRelative -import androidx.core.widget.doAfterTextChanged -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.flowWithLifecycle -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import by.kirich1409.viewbindingdelegate.viewBinding -import com.bumptech.glide.Glide -import com.github.terrakok.cicerone.Router -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.common.net.MediaType -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.VkUtils -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.model.attachments.VkAttachment -import com.meloda.fast.api.model.attachments.VkPhoto -import com.meloda.fast.api.model.domain.VkConversationDomain -import com.meloda.fast.base.viewmodel.BaseViewModelFragment -import com.meloda.fast.base.viewmodel.VkEvent -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.common.Screens -import com.meloda.fast.data.files.FilesRepository -import com.meloda.fast.databinding.DialogMessageDeleteBinding -import com.meloda.fast.databinding.FragmentMessagesHistoryBinding -import com.meloda.fast.ext.ImageLoader.loadWithGlide -import com.meloda.fast.ext.clear -import com.meloda.fast.ext.doOnApplyWindowInsets -import com.meloda.fast.ext.dpToPx -import com.meloda.fast.ext.getParcelableCompat -import com.meloda.fast.ext.gone -import com.meloda.fast.ext.hideKeyboard -import com.meloda.fast.ext.listenValue -import com.meloda.fast.ext.mimeType -import com.meloda.fast.ext.orDots -import com.meloda.fast.ext.sdk30AndUp -import com.meloda.fast.ext.selectLast -import com.meloda.fast.ext.showKeyboard -import com.meloda.fast.ext.trimmedText -import com.meloda.fast.ext.visible -import com.meloda.fast.model.base.parseString -import com.meloda.fast.screens.settings.SettingsFragment -import com.meloda.fast.util.AndroidUtils -import com.meloda.fast.util.ColorUtils -import com.meloda.fast.util.ShareContent -import com.meloda.fast.util.TimeUtils -import com.meloda.fast.view.SpaceItemDecoration -import dev.chrisbanes.insetter.applyInsetter -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.koin.android.ext.android.inject -import org.koin.androidx.viewmodel.ext.android.viewModel -import java.io.File -import java.text.SimpleDateFormat -import java.util.Locale -import java.util.Timer -import kotlin.concurrent.schedule -import kotlin.math.abs -import kotlin.properties.Delegates -import kotlin.random.Random - -class MessagesHistoryFragment : - BaseViewModelFragment(R.layout.fragment_messages_history) { - - private val router: Router by inject() - - private val binding by viewBinding(FragmentMessagesHistoryBinding::bind) - override val viewModel: MessagesHistoryViewModel by viewModel() - - private var pickFile: Boolean = false - - private val attachmentsToLoad = mutableListOf() - - private val getContent = - registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uriList: List? -> - if (uriList.isNullOrEmpty()) { - return@registerForActivityResult - } - - if (uriList.size > 10) { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.warning) - .setMessage("Select no more than 10 files") - .setPositiveButton(R.string.ok, null) - .show() - return@registerForActivityResult - } - - viewLifecycleOwner.lifecycleScope.launch { - val uploadFlow = flow { - uriList.forEach { uri -> - processFileFromStorage(uri) - emit(null) - } - } - - uploadFlow.collect() - } - } - - - private val actionState = MutableStateFlow(Action.RECORD) - - private enum class Action { - RECORD, SEND, EDIT, DELETE - } - - private val user: VkUser? by lazy { - requireArguments().getParcelableCompat(ARG_USER, VkUser::class.java) - } - - private val group: VkGroup? by lazy { - requireArguments().getParcelableCompat(ARG_GROUP, VkGroup::class.java) - } - - private var conversation: VkConversationDomain by Delegates.notNull() - - private val adapter: MessagesHistoryAdapter by lazy { - MessagesHistoryAdapter(this, conversation).also { - it.itemClickListener = this::onItemClick - it.avatarLongClickListener = this::onAvatarLongClickListener - } - } - - private val attachmentsAdapter: AttachmentsAdapter by lazy { - AttachmentsAdapter( - requireContext(), - emptyList(), - onRemoveClickedListener = { position -> - removeAttachment(attachmentsAdapter[position]) - } - ) - } - - private var timestampTimer: Timer? = null - - private lateinit var attachmentController: AttachmentPanelController - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - conversation = requireNotNull( - requireArguments().getParcelableCompat( - ARG_CONVERSATION, - VkConversationDomain::class.java - ) - ) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val colorBackground = ContextCompat.getColor(requireContext(), R.color.colorBackground) - val alphaColorBackground = ColorUtils.alphaColor(colorBackground, 0.85F) - binding.bottomMessagePanel.setBackgroundColor(alphaColorBackground) - - binding.toolbar.startButtonClickAction = { - requireActivity().onBackPressedDispatcher.onBackPressed() - } - - attachmentController = AttachmentPanelController.init( - context = requireContext(), - adapter = adapter, - lifecycleOwner = viewLifecycleOwner, - binding = binding, - isAttachmentsEmpty = { attachmentsToLoad.isEmpty() } - ) - - val title = when { - conversation.isChat() -> conversation.conversationTitle - conversation.isUser() -> user?.toString() - conversation.isGroup() -> group?.name - else -> null - } - -// listOf( -// binding.bottomAlpha, -// binding.bottomGradient -// ).forEach { v -> -// v.applyInsetter { -// type(navigationBars = true) { padding() } -// } -// } - binding.bottomMessagePanel.applyInsetter { - type(navigationBars = true, ime = true) { padding(animated = true) } - } -// binding.recyclerView.applyInsetter { -// type(navigationBars = true, ime = true) { padding(animated = true) } -// } - binding.toolbar.applyInsetter { - type(statusBars = true) { padding() } - } - binding.toolbar.title = title.orDots() - binding.toolbar.setOnClickListener { - openChatInfoScreen(conversation, user, group) - } - - val status = when { - conversation.isChat() -> "${conversation.membersCount} members" - conversation.isUser() -> when { - // TODO: 9/15/2021 user normal time - user?.online == true -> "Online" - user?.lastSeen != null -> "Last seen at ${ - SimpleDateFormat( - "HH:mm", - Locale.getDefault() - ).format(user?.lastSeen!! * 1000L) - }" - - else -> if (user?.lastSeenStatus != null) "Last seen ${user?.lastSeenStatus!!}" else "Last seen recently" - } - - conversation.isGroup() -> if (group?.membersCount != null) "${group?.membersCount} members" else "Group" - else -> null - } - - binding.toolbar.subtitle = status.orDots() - - prepareAvatar() - - prepareViews() - - binding.recyclerView.adapter = adapter - - viewModel.loadHistory(conversation.id) - - binding.action.setOnClickListener { - performAction() - } - -// binding.recyclerView.addOnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom -> -// if (bottom >= oldBottom) return@addOnLayoutChangeListener -// checkIfNeedToScrollToBottom() -// } - - binding.unreadCounter.setOnClickListener { - binding.recyclerView.scrollToPosition(adapter.lastPosition) - } - - binding.recyclerView.setItemViewCacheSize(30) - - binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - val layoutManager = recyclerView.layoutManager as LinearLayoutManager - val firstPosition = layoutManager.findFirstVisibleItemPosition() - val lastPosition = layoutManager.findLastCompletelyVisibleItemPosition() - - if (AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_FEATURES_HIDE_KEYBOARD_ON_SCROLL, - true - ) && dy < 0 - ) { - binding.recyclerView.hideKeyboard() - } - - setUnreadCounterVisibility(lastPosition, dy) - - adapter.getOrNull(firstPosition)?.let { - binding.timestamp.visible() - - val showExactTime = AppGlobal.preferences.getBoolean(SettingsFragment.KEY_SHOW_EXACT_TIME_ON_TIME_STAMP, false) - - val exactTime = SimpleDateFormat( - "HH:mm", - Locale.getDefault() - ).format(it.date * 1000L) - - val time = "${ - TimeUtils.getLocalizedDate( - requireContext(), - it.date * 1000L - ) - }${if (showExactTime) ", $exactTime" else ""}" - - binding.timestamp.text = time - - if (timestampTimer != null) { - timestampTimer?.cancel() - timestampTimer = null - } - - timestampTimer = Timer() - timestampTimer?.schedule(2500) { - recyclerView.post { - if (getView() == null) return@post - binding.timestamp.gone() - } - } - } - - super.onScrolled(recyclerView, dx, dy) - } - }) - - binding.message.doAfterTextChanged { text -> - val canSend = text.toString().isNotBlank() || attachmentsToLoad.isNotEmpty() - - val newValue: Action = - when { - attachmentController.isEditing -> - if (text.isNullOrBlank() && attachmentsToLoad.isEmpty()) { - Action.DELETE - } else { - Action.EDIT - } - - canSend -> Action.SEND - else -> { - Action.RECORD - } - } - - actionState.update { newValue } - } - - actionState - .asStateFlow() - .flowWithLifecycle(lifecycle) - .onEach { state -> - binding.action.animate() - .scaleX(1.25f) - .scaleY(1.25f) - .setDuration(100) - .withEndAction { - if (getView() == null) return@withEndAction - - binding.action.animate() - .scaleX(1f) - .scaleY(1f) - .setDuration(100) - .start() - }.start() - - when (state) { - Action.RECORD -> { - binding.action.setImageResource(R.drawable.ic_round_mic_24) - } - - Action.SEND -> { - binding.action.setImageResource(R.drawable.ic_round_send_24) - } - - Action.EDIT -> { - binding.action.setImageResource(R.drawable.ic_round_done_24) - } - - Action.DELETE -> { - binding.action.setImageResource(R.drawable.ic_trash_can_outline_24) - } - } - } - .launchIn(lifecycleScope) - - attachmentController.isPanelVisible.listenValue { isVisible -> - if (isVisible) binding.message.setSelection(binding.message.text.toString().length) - -// val currentHeight = binding.listAnchor.height -// -// val newHeight = -// if (isVisible) (binding.attachmentPanel.measuredHeight / 1.5).roundToInt() -// else 1 -// -// ValueAnimator.ofInt(currentHeight, newHeight).apply { -// duration = ATTACHMENT_PANEL_ANIMATION_DURATION -// interpolator = LinearInterpolator() -// -// addUpdateListener { animator -> -// if (getView() == null) return@addUpdateListener -// val value = animator.animatedValue as Int -// -// binding.listAnchor.updateLayoutParams { -// height = value -// } -// } -// }.start() - } - - binding.replyMessage.setOnClickListener { - val message = attachmentController.message.value ?: return@setOnClickListener - val index = adapter.searchMessageIndex(message.id) ?: return@setOnClickListener - - binding.recyclerView.scrollToPosition(index) - } - - binding.dismissReply.setOnClickListener { - if (attachmentController.message.value != null) - attachmentController.message.update { null } - } - - binding.attach.setOnClickListener { - showAttachmentsPopupMenu() - } - - binding.attach.setOnLongClickListener { - pickPhoto() - true - } - } - - override fun onEvent(event: VkEvent) { - super.onEvent(event) - - when (event) { - is MessagesMarkAsImportantEvent -> markMessagesAsImportant(event) - is MessagesLoadedEvent -> refreshMessages(event) - is MessagesPinEvent -> conversation.pinnedMessage = event.message - is MessagesUnpinEvent -> conversation.pinnedMessage = null - is MessagesDeleteEvent -> deleteMessages(event) - is MessagesEditEvent -> editMessage(event) - is MessagesReadEvent -> readMessages(event) - is MessagesNewEvent -> addNewMessage(event) - } - } - - private fun checkIfNeedToScrollToBottom() { - if (adapter.isEmpty()) return - - val lastVisiblePosition = - (binding.recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition() - - if (lastVisiblePosition <= adapter.lastPosition - 10) return - - binding.recyclerView.postDelayed({ - if (view == null) return@postDelayed - binding.recyclerView.smoothScrollToPosition(adapter.lastPosition) - }, 0) - } - - private suspend fun processFileFromStorage(uri: Uri) { - var name = "" - var size = 0.0 - - val contentResolver = requireContext().contentResolver - contentResolver.query(uri, null, null, null, null)?.use { cursor -> - val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) - - cursor.moveToFirst() - name = cursor.getString(nameIndex) - size = AndroidUtils.bytesToMegabytes(cursor.getLong(sizeIndex).toDouble()) - cursor.close() - } - - if (size > 200) { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.warning) - .setMessage("Selected file weighs more than 200 megabytes. Compress it or send other file") - .setPositiveButton(R.string.ok, null) - .setCancelable(false) - .show() - return - } - - val lastDotIndex = name.lastIndexOf(".") - var extension = if (lastDotIndex == -1) "" else name.substring(lastDotIndex + 1) - - if (extension.endsWith("msi") || extension.endsWith("exe") || extension.endsWith("apk")) { - extension += "fast" - name += "fast" - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.warning) - .setMessage("Selected file is executable. Fast changed it extension to \"$extension\", so the final name is \"$name\"") - .setPositiveButton(R.string.ok, null) - .setCancelable(false) - .show() - } - - val destination = requireContext() - .getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + - "${File.separator}upload.$extension" - - val file = File(destination) - if (file.exists()) file.delete() - - withContext(Dispatchers.IO) { - val inputStream = - requireActivity().contentResolver.openInputStream(uri) ?: return@withContext - - inputStream.use { input -> - file.outputStream().use { output -> - input.copyTo(output) - } - } - } - - val mimeType = contentResolver.getType(uri) ?: return - - if (pickFile) { - val uploadedAttachment = viewModel.uploadFile( - conversation.id, - file, - name, - FilesRepository.FileType.File - ) - addAttachment(uploadedAttachment) - } else { - when (MediaType.parse(mimeType).type()) { - MediaType.ANY_IMAGE_TYPE.type() -> { - val uploadedAttachment = viewModel.uploadPhoto(conversation.id, file, name) - addAttachment(uploadedAttachment) - } - - MediaType.ANY_VIDEO_TYPE.type() -> { - val uploadedAttachment = viewModel.uploadVideo(file, name) - addAttachment(uploadedAttachment) - } - - MediaType.ANY_AUDIO_TYPE.type() -> { - val uploadedAttachment = viewModel.uploadAudio(file, name) - addAttachment(uploadedAttachment) - } - } - } - } - - private fun showAttachmentsPopupMenu() { - val popupMenu = PopupMenu(requireContext(), binding.attach) - - if (attachmentsToLoad.isNotEmpty()) { - popupMenu.menu.add("Clear attachments") - } - - popupMenu.menu.add("Photo") - popupMenu.menu.add("Video") - popupMenu.menu.add("Audio") - popupMenu.menu.add("File") - popupMenu.setOnMenuItemClickListener { menuItem -> - return@setOnMenuItemClickListener when (menuItem.title) { - "Clear attachments" -> { - clearAttachments() - true - } - - "Photo" -> { - pickPhoto() - true - } - - "Video" -> { - pickVideo() - true - } - - "Audio" -> { - pickAudio() - true - } - - "File" -> { - pickFile() - true - } - - else -> false - } - } - popupMenu.show() - } - - private fun addAttachment(attachment: VkAttachment) { - attachmentsToLoad += attachment - binding.attachmentsCounter.visible() - binding.attachmentsCounter.text = attachmentsToLoad.size.toString() - - binding.attachmentsList.visible() - attachmentsAdapter.add(attachment) - - attachmentController.showPanel() - - actionState.value = - if (attachmentController.isEditing) Action.EDIT - else Action.SEND - } - - private fun removeAttachment(attachment: VkAttachment) { - attachmentsToLoad -= attachment - binding.attachmentsCounter.visible() - binding.attachmentsCounter.text = attachmentsToLoad.size.toString() - - binding.attachmentsList.visible() - - attachmentController.showPanel() - - if (attachmentsToLoad.isEmpty()) { - clearAttachments() - } else { - attachmentsAdapter.remove(attachment) - } - } - - private fun clearAttachments() { - attachmentsToLoad.clear() - binding.attachmentsCounter.gone() - binding.attachmentsCounter.text = null - - attachmentsAdapter.clear() - binding.attachmentsList.gone() - - attachmentController.hidePanel() - } - - private fun pickPhoto() { - getContent.launch(MediaType.ANY_IMAGE_TYPE.mimeType) - } - - private fun pickVideo() { - getContent.launch(MediaType.ANY_VIDEO_TYPE.mimeType) - } - - private fun pickAudio() { - getContent.launch(MediaType.MPEG_AUDIO.mimeType) - } - - private fun pickFile() { - pickFile = true - getContent.launch(MediaType.ANY_TYPE.mimeType) - } - - fun scrollToMessage(messageId: Int) { - adapter.searchMessageIndex(messageId)?.let { index -> - binding.recyclerView.scrollToPosition(index) - } - } - - private fun prepareAvatar() { - val avatar = when { - conversation.isUser() -> user?.photo200 - conversation.isGroup() -> group?.photo200 - conversation.isChat() -> conversation.conversationPhoto - else -> null - } - - val avatarImageView = binding.toolbar.avatarImageView - avatarImageView.visible() - avatarImageView.loadWithGlide { - imageUrl = avatar - asCircle = true - crossFade = true - } - } - - private fun performAction() { - when (actionState.value) { - Action.RECORD -> { - sdk30AndUp { - binding.action.performHapticFeedback(HapticFeedbackConstants.REJECT) - } - } - - Action.SEND -> { - val messageText = binding.message.trimmedText - if (messageText.isBlank() && attachmentsToLoad.isEmpty()) { - Log.d( - "MessagesHistoryFragment", - "performAction: SEND: messageText is empty & attachments is empty. return" - ) - return - } - - val date = System.currentTimeMillis() - - val messageIndex = adapter.lastPosition - - val attachments = attachmentsToLoad.ifEmpty { null }?.toList() - clearAttachments() - - val message = VkMessage( - id = Int.MAX_VALUE, - text = messageText, - isOut = true, - peerId = conversation.id, - fromId = UserConfig.userId, - date = (date / 1000).toInt(), - randomId = Random.nextInt(), - replyMessage = attachmentController.message.value, - attachments = attachments, - ).also { - it.state = VkMessage.State.Sending - } - - Log.d("LongPollUpdatesParser", "newMessageRandomId: ${message.randomId}") - - adapter.add(message, commitCallback = { - binding.recyclerView.scrollToPosition(adapter.lastPosition) - binding.message.clear() - }) - - val replyMessage = attachmentController.message.value - attachmentController.message.update { null } - - sdk30AndUp { - binding.action.performHapticFeedback(HapticFeedbackConstants.CONFIRM) - } - - viewModel.sendMessage( - peerId = conversation.id, - message = messageText.ifBlank { null }, - randomId = message.randomId, - replyTo = replyMessage?.id, - setId = { messageId -> - val messageToUpdate = adapter[messageIndex] - messageToUpdate.id = messageId - messageToUpdate.state = VkMessage.State.Sent - adapter.notifyItemChanged(messageIndex, "kek") -// adapter[messageIndex] = messageToUpdate - attachmentsAdapter.clear() - }, - onError = { - val messageToUpdate = adapter[messageIndex] - messageToUpdate.state = VkMessage.State.Error - adapter.notifyItemChanged(messageIndex, "kek") -// adapter[messageIndex] = messageToUpdate - attachmentsAdapter.clear() - }, - attachments = attachments - ) - } - - Action.EDIT -> { - val message = attachmentController.message.value ?: return - val messageText = binding.message.text.toString().trim() - - attachmentController.message.update { null } - - viewModel.editMessage( - originalMessage = message, - peerId = conversation.id, - messageId = message.id, - message = messageText, - attachments = message.attachments - ) - } - - Action.DELETE -> attachmentController.message.value?.let { - showDeleteMessageDialog(it) - } - } - } - - private fun prepareViews() { - prepareRecyclerView() - prepareEmojiButton() - prepareAttachmentsList() - } - - private fun prepareRecyclerView() { - binding.recyclerView.itemAnimator = null - - binding.toolbar.measure( - View.MeasureSpec.AT_MOST, - View.MeasureSpec.UNSPECIFIED - ) - - binding.bottomMessagePanel.measure( - View.MeasureSpec.AT_MOST, - View.MeasureSpec.UNSPECIFIED - ) - -// binding.recyclerView.updatePaddingRelative( -// top = binding.toolbar.measuredHeight, -// bottom = binding.bottomMessagePanel.measuredHeight -// ) - - val toolbarMeasuredHeight = binding.toolbar.measuredHeight - val bottomMessagePanelMeasuredHeight = binding.bottomMessagePanel.measuredHeight - - binding.recyclerView.doOnApplyWindowInsets { v, insets, _, _ -> - val statusBars = AndroidUtils.getStatusBarInsets(insets) - val ime = AndroidUtils.getImeInsets(insets) - val navBars = AndroidUtils.getNavBarInsets(insets) - - val topPadding = toolbarMeasuredHeight + statusBars.top - - val bottomPadding = bottomMessagePanelMeasuredHeight + - ime.bottom + (if (ime.bottom == 0) navBars.bottom else 0) - - val currentPadding = v.paddingBottom - - v.updatePaddingRelative(top = topPadding) - ValueAnimator.ofInt(currentPadding, bottomPadding).apply { - interpolator = LinearInterpolator() - duration = if (currentPadding > bottomPadding) 125 else 50 - - addUpdateListener { - if (view == null) return@addUpdateListener - val value = it.animatedValue as Int - v.updatePaddingRelative(bottom = value) - } - - doOnEnd { - if (view == null) return@doOnEnd - checkIfNeedToScrollToBottom() - } - }.start() - -// v.updatePaddingRelative(top = topPadding, bottom = bottomPadding) - - insets - } - } - - private fun prepareEmojiButton() { - binding.emoji.setOnClickListener { - sdk30AndUp { - binding.emoji.performHapticFeedback(HapticFeedbackConstants.REJECT) - } - } - binding.emoji.setOnLongClickListener { - val text = binding.message.text.toString() + - AppGlobal.preferences.getString( - SettingsFragment.KEY_FEATURES_FAST_TEXT, - SettingsFragment.DEFAULT_VALUE_FEATURES_FAST_TEXT - ) - binding.message.setText(text) - binding.message.selectLast() - - binding.emoji.animate() - .scaleX(1.25f) - .scaleY(1.25f) - .setDuration(100) - .withEndAction { - if (view == null) return@withEndAction - - binding.emoji.animate() - .scaleX(1f) - .scaleY(1f) - .setDuration(100) - .start() - }.start() - - binding.emoji.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - true - } - } - - private fun prepareAttachmentsList() { - binding.attachmentsList.addItemDecoration( - SpaceItemDecoration(endMargin = 4.dpToPx()) - ) - binding.attachmentsList.adapter = attachmentsAdapter - } - - private fun markMessagesAsImportant(event: MessagesMarkAsImportantEvent) { - val newList = adapter.cloneCurrentList() - - for (i in newList.indices) { - val message = newList[i] - if (event.messagesIds.contains(message.id)) { - newList[i] = message.copy(important = event.important) - } - } - - adapter.submitList(newList) - } - - private fun refreshMessages(event: MessagesLoadedEvent) { - adapter.profiles += event.profiles - adapter.groups += event.groups - - fillRecyclerView(event.messages) - } - - private fun fillRecyclerView(values: List) { - val smoothScroll = adapter.isNotEmpty() - - adapter.setItems( - values.sortedBy { it.date }, - commitCallback = { - if (view == null) return@setItems - if (smoothScroll) binding.recyclerView.smoothScrollToPosition(adapter.lastPosition) - else binding.recyclerView.scrollToPosition(adapter.lastPosition) - } - ) - } - - private fun onItemClick(position: Int) { - showOptionsDialog(position) - } - - private fun onAvatarLongClickListener(position: Int) { - val message = adapter[position] - - val messageUser = VkUtils.getMessageUser(message, adapter.profiles) - val messageGroup = VkUtils.getMessageGroup(message, adapter.groups) - - val title = VkUtils.getMessageTitle(message, messageUser, messageGroup) - Toast.makeText(requireContext(), title, Toast.LENGTH_SHORT).show() - } - - private fun showOptionsDialog(position: Int) { - val message = adapter[position] - if (message.action != null) return - - val time = getString( - R.string.time_format, - SimpleDateFormat( - "dd.MM.yyyy, HH:mm:ss", - Locale.getDefault() - ).format(message.date * 1000L) - ) - - val reply = getString(R.string.message_context_action_reply) - - val isMessageAlreadyPinned = message.id == conversation.pinnedMessage?.id - - val pin = getString( - if (isMessageAlreadyPinned) R.string.message_context_action_unpin - else R.string.message_context_action_pin - ) - - val important = getString( - if (message.important) R.string.message_context_action_unmark_as_important - else R.string.message_context_action_mark_as_important - ) - - val read = "Mark as read" - - val edit = getString(R.string.message_context_action_edit) - - val copy = "Copy" - - val share = "Share" - - val delete = getString(R.string.message_context_action_delete) - - val params = mutableListOf() - val onlySentParams = mutableListOf() - - params += reply - onlySentParams += reply - - if (conversation.canChangePin) { - params += pin - onlySentParams += pin - } - - params += important - onlySentParams += important - - if (!message.isRead(conversation) && !message.isOut) { - params += read - onlySentParams += read - } - - if (message.canEdit()) { - params += edit - onlySentParams += edit - } - - val notNullText = message.text.orEmpty() - val messageTextIsNotNull = !message.text.isNullOrBlank() - - val notNullAttachments = message.attachments.orEmpty() - val attachmentsIsOnePhoto = notNullAttachments.size == 1 && - notNullAttachments.first() is VkPhoto - - if (messageTextIsNotNull || attachmentsIsOnePhoto) { - params += copy - params += share - } - - params += delete - - if (!message.isSent()) { - params.removeAll(onlySentParams) - } - - val arrayParams = params.toTypedArray() - - MaterialAlertDialogBuilder(requireContext()) - .setTitle(time) - .setItems(arrayParams) { _, which -> - when (params[which]) { - reply -> { - if (attachmentController.message.value != message) - attachmentController.message.update { message } - } - - pin -> showPinMessageDialog( - peerId = conversation.id, - messageId = message.id, - pin = !isMessageAlreadyPinned - ) - - important -> viewModel.markAsImportant( - messagesIds = listOf(message.id), - important = !message.important - ) - - read -> viewModel.readMessage( - conversation.id, - message.id - ) - - edit -> { - attachmentController.isEditing = true - - if (attachmentController.message.value != message) - attachmentController.message.update { message } - } - - copy -> { - lifecycleScope.launch(Dispatchers.IO) { - when { - messageTextIsNotNull && !attachmentsIsOnePhoto -> { - withContext(Dispatchers.Main) { - AndroidUtils.copyText( - text = notNullText, - withToast = true - ) - } - } - - else -> { - val imageUrl = - ((notNullAttachments.first() as? VkPhoto)?.getMaxSize() - ?: return@launch).url - - val preloadedImageFileUri = Glide - .with(requireContext()) - .downloadOnly() - .load(imageUrl) - .submit() - .get().let { file -> - val newFile = - AndroidUtils.getImageToShare(requireContext(), file) - - newFile!! - } - - withContext(Dispatchers.Main) { - if (messageTextIsNotNull) { - AndroidUtils.copyText(text = notNullText) - AndroidUtils.copyImage( - label = "Image", - imageUri = preloadedImageFileUri, - withToast = true - ) - } else { - AndroidUtils.copyImage( - label = "Image", - imageUri = preloadedImageFileUri, - withToast = true - ) - } - } - } - } - } - } - - share -> { - lifecycleScope.launch(Dispatchers.IO) { - val content = when { - messageTextIsNotNull && !attachmentsIsOnePhoto -> { - ShareContent.Text(notNullText) - } - - else -> { - val imageUrl = - ((notNullAttachments.first() as? VkPhoto)?.getMaxSize() - ?: return@launch).url - - val preloadedImageFileUri = Glide - .with(requireContext()) - .downloadOnly() - .load(imageUrl) - .submit() - .get().let { file -> - val newFile = - AndroidUtils.getImageToShare(requireContext(), file) - - newFile!! - } - - if (messageTextIsNotNull) { - ShareContent.TextWithImage( - notNullText, preloadedImageFileUri - ) - } else { - ShareContent.Image(preloadedImageFileUri) - } - } - } - - withContext(Dispatchers.Main) { - AndroidUtils.showShareSheet(requireActivity(), content) - } - } - } - - delete -> showDeleteMessageDialog(message) - } - }.show() - } - - private fun showPinMessageDialog( - peerId: Int, - messageId: Int?, - pin: Boolean, - ) { - MaterialAlertDialogBuilder(requireContext()) - .setTitle( - if (pin) R.string.confirm_pin_message - else R.string.confirm_unpin_message - ) - .setPositiveButton( - if (pin) R.string.action_pin - else R.string.action_unpin - ) { _, _ -> - viewModel.pinMessage( - peerId = peerId, - messageId = messageId, - pin = pin - ) - } - .setNegativeButton(R.string.cancel, null) - .show() - } - - private fun showDeleteMessageDialog(message: VkMessage) { - val binding = DialogMessageDeleteBinding.inflate(layoutInflater, null, false) - - binding.check.setText( - if (message.isOut || conversation.canChangeInfo) R.string.message_delete_for_all - else R.string.message_mark_as_spam - ) - - binding.check.isEnabled = - message.isSent() && ((conversation.id != UserConfig.userId) && (!message.isOut || message.canEdit())) - - if (message.isSent() && conversation.id == UserConfig.userId || - (binding.check.isEnabled && message.isOut) - ) binding.check.isChecked = true - - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.confirm_delete_message) - .setView(binding.root) - .setPositiveButton(R.string.action_delete) { _, _ -> - attachmentController.message.update { null } - - if (message.isError()) { - adapter.searchIndexOf(message)?.let { index -> - adapter.removeAt(index) - } - - return@setPositiveButton - } - - viewModel.deleteMessage( - peerId = conversation.id, - messagesIds = listOf(message.id), - isSpam = if (message.isOut) null else binding.check.isChecked, - deleteForAll = if (!binding.check.isEnabled) null else binding.check.isChecked - ) - } - .setNegativeButton(R.string.cancel, null) - .show() - } - - private fun deleteMessages(event: MessagesDeleteEvent) { - if (event.peerId != conversation.id) return - val messagesToDelete = - event.messagesIds.mapNotNull { id -> adapter.searchMessageById(id) } - adapter.removeAll(messagesToDelete) - } - - private fun editMessage(event: MessagesEditEvent) { - if (event.message.peerId != conversation.id) return - adapter.searchMessageIndex(event.message.id)?.let { index -> - adapter[index] = event.message - adapter.notifyItemChanged(index) - } - } - - private fun readMessages(event: MessagesReadEvent) { - if (event.peerId != conversation.id) return - - val oldOutRead = conversation.outRead - val oldInRead = conversation.inRead - - conversation = conversation.copy( - outRead = if (event.isOut) event.messageId else conversation.outRead, - inRead = if (!event.isOut) event.messageId else conversation.inRead - ) - - val positionsToUpdate = mutableListOf() - val newList = adapter.cloneCurrentList() - for (i in newList.indices) { - val message = newList[i] - - if ((message.isOut && conversation.outRead - oldOutRead > 0 && message.id > oldOutRead) || - (!message.isOut && conversation.inRead - oldInRead > 0 && message.id > oldInRead) - ) { - positionsToUpdate += i - } - } - - positionsToUpdate.forEach { index -> - adapter.notifyItemChanged(index) - - if (binding.unreadCounter.isVisible) { - setUnreadCounterVisibility( - (binding.recyclerView.layoutManager as LinearLayoutManager) - .findLastCompletelyVisibleItemPosition() - ) - } - } - } - - private fun setUnreadCounterVisibility( - lastCompletelyVisiblePosition: Int, - dy: Int? = null, - ) { - if (lastCompletelyVisiblePosition >= adapter.lastPosition - 1) { - setUnreadCounterVisibility(false) - } else { - if (adapter.containsUnreadMessages()) { - setUnreadCounterVisibility(true) - } else { - if (dy == null) { - setUnreadCounterVisibility(false) - } else { - if (dy > 0) { - if (dy > 40) setUnreadCounterVisibility(true) - } else { - if (dy < -40) setUnreadCounterVisibility(false) - } - } - } - } - } - - private fun addNewMessage(event: MessagesNewEvent) { - if (event.message.peerId != conversation.id) return - - adapter.profiles += event.profiles - adapter.groups += event.groups - - if (adapter.containsRandomId(event.message.randomId) - || adapter.containsId(event.message.id) - ) return - - val itemCount = adapter.itemCount - - adapter.add(event.message) { - if (view == null) return@add - - val lastVisiblePosition = - (binding.recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition() - - if (abs(lastVisiblePosition - adapter.lastPosition) <= 3) { - binding.recyclerView.scrollToPosition(adapter.lastPosition) - } else { - setUnreadCounterVisibility(true) - // add counter of unread - } - } - adapter.notifyItemRangeChanged(0, itemCount, "avatars") - } - - private fun setUnreadCounterVisibility(isVisible: Boolean) { - if (view == null) return - - binding.unreadCounter.run { - if (isVisible) { - show() - } else { - hide() - } - } - } - - private class AttachmentPanelController { - companion object { - fun init( - context: Context, - adapter: MessagesHistoryAdapter, - lifecycleOwner: LifecycleOwner, - binding: FragmentMessagesHistoryBinding, - isAttachmentsEmpty: () -> Boolean, - ): AttachmentPanelController { - val controller = AttachmentPanelController().apply { - this.context = context - this.binding = binding - this.adapter = adapter - this.isAttachmentsEmpty = isAttachmentsEmpty - this.message.listenValue( - lifecycleOwner.lifecycleScope, - this::onMessageValueChanged - ) - - this.message.update { null } - } - - - return controller - } - } - - val isPanelVisible = MutableStateFlow(false) - val message = MutableStateFlow(null) - - var isEditing = false - - var adapter: MessagesHistoryAdapter by Delegates.notNull() - var binding: FragmentMessagesHistoryBinding by Delegates.notNull() - var context: Context by Delegates.notNull() - var isAttachmentsEmpty: () -> Boolean by Delegates.notNull() - - fun onMessageValueChanged(value: VkMessage?) { - if (value != null) { - applyMessage(value) - } else { - clearMessage() - } - } - - private fun applyMessage(message: VkMessage) { - val messageUser: VkUser? = - if (message.isUser()) adapter.profiles[message.fromId] - else null - val messageGroup: VkGroup? = - if (message.isGroup()) adapter.groups[message.fromId] - else null - val title = VkUtils.getMessageTitle( - message, messageUser, messageGroup - ) - - val attachmentText = (if (message.text == null) VkUtils.getAttachmentText( - message = message - ) else null)?.parseString(context) - - val forwardsMessage = (if (message.text == null) VkUtils.getForwardsText( - message = message - ) else null)?.parseString(context) - - val messageText = forwardsMessage ?: attachmentText - ?: (message.text ?: "").run { VkUtils.prepareMessageText(this) } - - binding.replyMessageTitle.text = title - binding.replyMessageText.text = messageText - - if (isEditing) { - binding.message.setText(message.text) - binding.message.setSelection(message.text?.length ?: 0) - binding.message.requestFocusFromTouch() - binding.message.showKeyboard() - } - - binding.replyMessage.visible() - - showPanel() - } - - private fun clearMessage() { - if (isAttachmentsEmpty()) { - hidePanel() - } - - binding.replyMessage.gone() - - binding.replyMessageTitle.clear() - binding.replyMessageText.clear() - - if (isEditing) { - isEditing = false - binding.message.clear() - } - } - - fun showPanel() { - if (isPanelVisible.value) return - - binding.attachmentPanel.visible() -// binding.attachmentPanel.measure( -// View.MeasureSpec.AT_MOST, View.MeasureSpec.UNSPECIFIED -// ) - - if (!isPanelVisible.value) - isPanelVisible.update { true } - -// binding.attachmentPanel.visible() - -// val measuredHeight = binding.attachmentPanel.measuredHeight -// -// binding.attachmentPanel.updateLayoutParams { -// height = 0 -// } -// -// binding.attachmentPanel.animate() -// .translationY(0f) -// .setDuration(ATTACHMENT_PANEL_ANIMATION_DURATION) -// .start() -// -// ValueAnimator.ofInt(0, measuredHeight).apply { -// duration = ATTACHMENT_PANEL_ANIMATION_DURATION -// interpolator = LinearInterpolator() -// -// addUpdateListener { animator -> -// if (view == null) return@addUpdateListener -// val value = animator.animatedValue as Int -// -// if (value >= 36.dpToPx()) { -// binding.attachmentPanel.visible() -// } -// -// binding.attachmentPanel.updateLayoutParams { -// height = value -// } -// } -// }.start() - } - - fun hidePanel() { - if (!isPanelVisible.value || - !isAttachmentsEmpty() || - message.value != null - ) return - - if (isPanelVisible.value) - isPanelVisible.update { false } - - binding.attachmentPanel.gone() - -// val currentHeight = binding.attachmentPanel.height -// -// binding.attachmentPanel.animate() -// .translationY(75F) -// .setDuration(ATTACHMENT_PANEL_ANIMATION_DURATION) -// .start() -// -// ValueAnimator.ofInt(currentHeight, 0).apply { -// duration = ATTACHMENT_PANEL_ANIMATION_DURATION -// interpolator = LinearInterpolator() -// -// addUpdateListener { animator -> -// if (view == null) return@addUpdateListener -// val value = animator.animatedValue as Int -// -// if (value <= 36.dpToPx()) { -// binding.attachmentPanel.gone() -// } -// -// binding.attachmentPanel.updateLayoutParams { -// height = value -// } -// } -// doOnEnd { -// if (view == null) return@doOnEnd -// binding.attachmentPanel.gone() -// } -// }.start() - } - } - - fun openForwardsScreen( - conversation: VkConversationDomain, - messages: List, - profiles: HashMap = hashMapOf(), - groups: HashMap = hashMapOf(), - ) { - router.navigateTo( - Screens.ForwardedMessages(conversation, messages, profiles, groups) - ) - } - - private fun openChatInfoScreen( - conversation: VkConversationDomain, - user: VkUser?, - group: VkGroup?, - ) { - router.navigateTo( - Screens.ChatInfo(conversation, user, group) - ) - } - - companion object { - const val ARG_USER: String = "user" - const val ARG_GROUP: String = "group" - const val ARG_CONVERSATION: String = "conversation" - - private const val ATTACHMENT_PANEL_ANIMATION_DURATION = 150L - - fun newInstance( - conversation: VkConversationDomain, - user: VkUser?, - group: VkGroup?, - ): MessagesHistoryFragment { - val fragment = MessagesHistoryFragment() - fragment.arguments = bundleOf( - ARG_CONVERSATION to conversation, - ARG_USER to user, - ARG_GROUP to group - ) - - return fragment - } - } - -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt deleted file mode 100644 index ebc05fce..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt +++ /dev/null @@ -1,598 +0,0 @@ -package com.meloda.fast.screens.messages - -import com.meloda.fast.api.VKConstants -import com.meloda.fast.api.base.ApiError -import com.meloda.fast.api.longpoll.LongPollEvent -import com.meloda.fast.api.longpoll.LongPollUpdatesParser -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.model.attachments.VkAttachment -import com.meloda.fast.api.model.attachments.VkVideo -import com.meloda.fast.api.model.domain.VkConversationDomain -import com.meloda.fast.api.network.messages.MessagesDeleteRequest -import com.meloda.fast.api.network.messages.MessagesEditRequest -import com.meloda.fast.api.network.messages.MessagesGetHistoryRequest -import com.meloda.fast.api.network.messages.MessagesMarkAsImportantRequest -import com.meloda.fast.api.network.messages.MessagesPinMessageRequest -import com.meloda.fast.api.network.messages.MessagesSendRequest -import com.meloda.fast.api.network.messages.MessagesUnPinMessageRequest -import com.meloda.fast.api.network.photos.PhotosSaveMessagePhotoRequest -import com.meloda.fast.base.viewmodel.DeprecatedBaseViewModel -import com.meloda.fast.base.viewmodel.VkEvent -import com.meloda.fast.data.audios.AudiosRepository -import com.meloda.fast.data.files.FilesRepository -import com.meloda.fast.data.messages.MessagesRepository -import com.meloda.fast.data.photos.PhotosRepository -import com.meloda.fast.data.videos.VideosRepository -import com.meloda.fast.ext.notNull -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.MultipartBody -import okhttp3.RequestBody.Companion.asRequestBody -import java.io.File -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine - -class MessagesHistoryViewModel constructor( - private val messagesRepository: MessagesRepository, - updatesParser: LongPollUpdatesParser, - private val photosRepository: PhotosRepository, - private val filesRepository: FilesRepository, - private val audiosRepository: AudiosRepository, - private val videosRepository: VideosRepository, -) : DeprecatedBaseViewModel() { - - init { - updatesParser.onNewMessage { - launch { handleNewMessage(it) } - } - - updatesParser.onMessageEdited { - launch { handleEditedMessage(it) } - } - - updatesParser.onMessageIncomingRead { - launch { handleReadIncomingEvent(it) } - } - - updatesParser.onMessageOutgoingRead { - launch { handleReadOutgoingEvent(it) } - } - } - - private suspend fun handleNewMessage(event: LongPollEvent.VkMessageNewEvent) { - sendEvent(MessagesNewEvent(event.message, event.profiles, event.groups)) - } - - private suspend fun handleEditedMessage(event: LongPollEvent.VkMessageEditEvent) { - sendEvent(MessagesEditEvent(event.message)) - } - - private suspend fun handleReadIncomingEvent(event: LongPollEvent.VkMessageReadIncomingEvent) { - sendEvent( - MessagesReadEvent( - isOut = false, - peerId = event.peerId, - messageId = event.messageId - ) - ) - } - - private suspend fun handleReadOutgoingEvent(event: LongPollEvent.VkMessageReadOutgoingEvent) { - sendEvent( - MessagesReadEvent( - isOut = true, - peerId = event.peerId, - messageId = event.messageId - ) - ) - } - - fun loadHistory(peerId: Int) = launch { - makeJob({ - messagesRepository.getHistory( - MessagesGetHistoryRequest( - count = 100, - peerId = peerId, - extended = true, - fields = VKConstants.ALL_FIELDS - ) - ) - }, - onAnswer = { - val response = it.response ?: return@makeJob - - val profiles = hashMapOf() - response.profiles?.let { baseProfiles -> - baseProfiles.forEach { baseProfile -> - baseProfile.mapToDomain().let { profile -> profiles[profile.id] = profile } - } - } - - val groups = hashMapOf() - response.groups?.let { baseGroups -> - baseGroups.forEach { baseGroup -> - baseGroup.mapToDomain().let { group -> groups[group.id] = group } - } - } - - val hashMessages = hashMapOf() - response.items.forEach { baseMessage -> - baseMessage.asVkMessage() - .let { message -> hashMessages[message.id] = message } - } - - messagesRepository.store(hashMessages.values.toList()) - - val conversations = hashMapOf() - response.conversations?.let { baseConversations -> - baseConversations.forEach { baseConversation -> - baseConversation.mapToDomain( - hashMessages[baseConversation.last_message_id] - ).let { conversation -> conversations[conversation.id] = conversation } - } - } - - sendEvent( - MessagesLoadedEvent( - count = response.count, - profiles = profiles, - groups = groups, - conversations = conversations, - messages = hashMessages.values.toList() - ) - ) - }) - } - - fun sendMessage( - peerId: Int, - message: String? = null, - randomId: Int = 0, - replyTo: Int? = null, - setId: ((messageId: Int) -> Unit)? = null, - onError: ((error: Throwable) -> Unit)? = null, - attachments: List? = null, - ) = launch { - makeJob( - { - messagesRepository.send( - MessagesSendRequest( - peerId = peerId, - randomId = randomId, - message = message, - replyTo = replyTo, - attachments = attachments - ) - ) - }, - onAnswer = { - val response = it.response ?: return@makeJob - setId?.invoke(response) - }, - onError = { - onError?.invoke(it) - }) - } - - fun markAsImportant( - messagesIds: List, - important: Boolean, - ) = launch { - makeJob({ - messagesRepository.markAsImportant( - MessagesMarkAsImportantRequest( - messagesIds = messagesIds, - important = important - ) - ) - }, - onAnswer = { - val response = it.response ?: return@makeJob - sendEvent( - MessagesMarkAsImportantEvent( - messagesIds = response, - important = important - ) - ) - }) - } - - fun pinMessage( - peerId: Int, - messageId: Int? = null, - conversationMessageId: Int? = null, - pin: Boolean, - ) = launch { - if (pin) { - makeJob({ - messagesRepository.pin( - MessagesPinMessageRequest( - peerId = peerId, - messageId = messageId, - conversationMessageId = conversationMessageId - ) - ) - }, - onAnswer = { - val response = it.response ?: return@makeJob - sendEvent(MessagesPinEvent(response.asVkMessage())) - } - ) - } else { - makeJob({ messagesRepository.unpin(MessagesUnPinMessageRequest(peerId = peerId)) }, - onAnswer = { - println("Fast::MessagesHistoryViewModel::unPin::Response::${it.response}") - sendEvent(MessagesUnpinEvent) - } - ) - } - } - - fun deleteMessage( - peerId: Int, - messagesIds: List? = null, - conversationsMessagesIds: List? = null, - isSpam: Boolean? = null, - deleteForAll: Boolean? = null, - ) = launch { - makeJob( - { - messagesRepository.delete( - MessagesDeleteRequest( - peerId = peerId, - messagesIds = messagesIds, - conversationsMessagesIds = conversationsMessagesIds, - isSpam = isSpam, - deleteForAll = deleteForAll - ) - ) - }, - onAnswer = { - sendEvent( - MessagesDeleteEvent( - peerId = peerId, - messagesIds = messagesIds ?: emptyList() - ) - ) - }) - } - - fun editMessage( - originalMessage: VkMessage, - peerId: Int, - messageId: Int, - message: String? = null, - attachments: List? = null, - ) = launch { - makeJob( - { - messagesRepository.edit( - MessagesEditRequest( - peerId = peerId, - messageId = messageId, - message = message, - attachments = attachments - ) - ) - }, - onAnswer = { - originalMessage.text = message - sendEvent(MessagesEditEvent(originalMessage)) - } - ) - } - - fun readMessage(peerId: Int, messageId: Int) { - makeJob( - { messagesRepository.markAsRead(peerId, startMessageId = messageId) }, - onAnswer = { - sendEvent(MessagesReadEvent(false, peerId, messageId)) - } - ) - } - - suspend fun uploadPhoto( - peerId: Int, - photo: File, - name: String, - ) = suspendCoroutine { - launch { - val uploadServerUrl = getPhotoMessageUploadServer(peerId) - val uploadedFileInfo = uploadPhotoToServer(uploadServerUrl, photo, name) - - val savedAttachment = saveMessagePhoto( - uploadedFileInfo.first, - uploadedFileInfo.second, - uploadedFileInfo.third - ) - - it.resume(savedAttachment) - } - } - - private suspend fun getPhotoMessageUploadServer(peerId: Int) = - suspendCoroutine { continuation -> - launch { - sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { photosRepository.getMessagesUploadServer(peerId) } - ).response?.let { response -> - continuation.resume(response.uploadUrl) - } - } - } - - private suspend fun uploadPhotoToServer( - uploadUrl: String, - photo: File, - name: String, - ) = suspendCoroutine { continuation -> - launch { - val requestBody = photo.asRequestBody("image/*".toMediaType()) - val body = MultipartBody.Part.createFormData("photo", name, requestBody) - - sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { photosRepository.uploadPhoto(uploadUrl, body) } - ).let { response -> - continuation.resume(Triple(response.server, response.photo, response.hash)) - } - } - } - - private suspend fun saveMessagePhoto( - server: Int, - photo: String, - hash: String, - ) = suspendCoroutine { continuation -> - launch { - sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { - photosRepository.saveMessagePhoto( - PhotosSaveMessagePhotoRequest(photo, server, hash) - ) - } - ).response?.first()?.asVkPhoto()?.let(continuation::resume) - } - } - - suspend fun uploadVideo( - file: File, - name: String, - ) = suspendCoroutine { - launch { - val uploadInfo = getVideoMessageUploadServer() - - uploadVideoToServer( - uploadInfo.first, - file, - name - ) - - it.resume(uploadInfo.second) - } - } - - private suspend fun getVideoMessageUploadServer() = suspendCoroutine { continuation -> - launch { - sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { videosRepository.save() } - ).response?.let { response -> - val uploadUrl = response.uploadUrl - val video = VkVideo( - id = response.videoId, - ownerId = response.ownerId, - images = emptyList(), - firstFrames = null, - accessKey = response.accessKey, - title = response.title - ) - - continuation.resume(uploadUrl to video) - } - } - } - - private suspend fun uploadVideoToServer( - uploadUrl: String, - file: File, - name: String, - ) = launch { - val requestBody = file.asRequestBody() - val body = MultipartBody.Part.createFormData("video_file", name, requestBody) - - sendRequest( - onError = { exception -> throw exception }, - request = { videosRepository.upload(uploadUrl, body) } - ) - } - - suspend fun uploadAudio( - file: File, - name: String, - ) = suspendCoroutine { - launch { - val uploadUrl = getAudioUploadServer() - val uploadInfo = uploadAudioToServer(uploadUrl, file, name) - val saveInfo = saveMessageAudio( - uploadInfo.first, uploadInfo.second, uploadInfo.third - ) - - it.resume(saveInfo) - } - } - - private suspend fun getAudioUploadServer() = suspendCoroutine { continuation -> - launch { - sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { audiosRepository.getUploadServer() } - ).response?.uploadUrl?.let(continuation::resume) - } - } - - private suspend fun uploadAudioToServer( - uploadUrl: String, - file: File, - name: String, - ) = suspendCoroutine { continuation -> - launch { - val requestBody = file.asRequestBody() - val body = MultipartBody.Part.createFormData("file", name, requestBody) - - sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { audiosRepository.upload(uploadUrl, body) } - ).let { response -> - response.error?.let { error -> throw ApiError(error = error) } - - continuation.resume( - Triple(response.server, response.audio.notNull(), response.hash) - ) - } - } - } - - private suspend fun saveMessageAudio( - server: Int, - audio: String, - hash: String, - ) = suspendCoroutine { continuation -> - launch { - sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { audiosRepository.save(server, audio, hash) } - ).response?.asVkAudio()?.let(continuation::resume) - } - } - - suspend fun uploadFile( - peerId: Int, - file: File, - name: String, - type: FilesRepository.FileType, - ) = suspendCoroutine { continuation -> - launch { - val uploadServerUrl = getFileMessageUploadServer(peerId, type) - val uploadedFileInfo = uploadFileToServer(uploadServerUrl, file, name) - val savedAttachmentPair = saveMessageFile(uploadedFileInfo) - - continuation.resume(savedAttachmentPair.second) - } - } - - private suspend fun getFileMessageUploadServer( - peerId: Int, - type: FilesRepository.FileType, - ) = suspendCoroutine { continuation -> - launch { - val uploadServerResponse = sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { filesRepository.getMessagesUploadServer(peerId, type) } - ).response.notNull() - - continuation.resume(uploadServerResponse.uploadUrl) - } - } - - private suspend fun uploadFileToServer( - uploadUrl: String, - file: File, - name: String, - ) = suspendCoroutine { continuation -> - launch { - val requestBody = file.asRequestBody() - val body = MultipartBody.Part.createFormData("file", name, requestBody) - - sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { filesRepository.uploadFile(uploadUrl, body) } - ).let { response -> - response.error?.let { error -> throw ApiError(error = error) } - - continuation.resume(response.file.notNull()) - } - } - } - - private suspend fun saveMessageFile(file: String) = - suspendCoroutine { continuation -> - launch { - sendRequestNotNull( - onError = { exception -> - continuation.resumeWithException(exception) - true - }, - request = { filesRepository.saveMessageFile(file) } - ).response?.let { response -> - val type = response.type - val attachmentFile = - response.file?.asVkFile() ?: response.voiceMessage?.asVkVoiceMessage() - - continuation.resume(type to attachmentFile.notNull()) - } - } - } -} - -data class MessagesLoadedEvent( - val count: Int, - val conversations: HashMap, - val messages: List, - val profiles: HashMap, - val groups: HashMap, -) : VkEvent() - -data class MessagesMarkAsImportantEvent(val messagesIds: List, val important: Boolean) : - VkEvent() - -data class MessagesPinEvent(val message: VkMessage) : VkEvent() - -object MessagesUnpinEvent : VkEvent() - -data class MessagesDeleteEvent(val peerId: Int, val messagesIds: List) : VkEvent() - -data class MessagesEditEvent(val message: VkMessage) : VkEvent() - -data class MessagesReadEvent( - val isOut: Boolean, - val peerId: Int, - val messageId: Int, -) : VkEvent() - -data class MessagesNewEvent( - val message: VkMessage, - val profiles: HashMap, - val groups: HashMap, -) : VkEvent() diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt deleted file mode 100644 index 74bfb33e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt +++ /dev/null @@ -1,303 +0,0 @@ -package com.meloda.fast.screens.messages - -import android.content.Context -import android.content.res.ColorStateList -import android.graphics.drawable.ColorDrawable -import android.text.method.LinkMovementMethod -import android.util.Log -import android.view.View -import android.widget.* -import androidx.appcompat.widget.LinearLayoutCompat -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.content.ContextCompat -import androidx.core.view.isVisible -import androidx.core.view.updateLayoutParams -import com.meloda.fast.R -import com.meloda.fast.api.VkUtils -import com.meloda.fast.api.model.domain.VkConversationDomain -import com.meloda.fast.api.model.VkGroup -import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.ext.clear -import com.meloda.fast.ext.dpToPx -import com.meloda.fast.ext.gone -import com.meloda.fast.ext.orDots -import com.meloda.fast.ext.toggleVisibility -import com.meloda.fast.ext.visible -import com.meloda.fast.ext.ImageLoader.loadWithGlide -import java.text.SimpleDateFormat -import java.util.* - - -class MessagesPreparator constructor( - private val context: Context, - - private val position: Int, - - private val adapterClickListener: ((position: Int) -> Unit)? = null, - - private val payloads: MutableList? = null, - - private val root: View? = null, - - private val conversation: VkConversationDomain, - private val message: VkMessage, - private val prevMessage: VkMessage? = null, - private val nextMessage: VkMessage? = null, - - private val bubble: ConstraintLayout, - private val text: TextView? = null, - private val avatar: ImageView? = null, - private val title: TextView? = null, - private val spacer: Space? = null, - private val messageState: ImageView? = null, - private val time: TextView? = null, - private val replyContainer: FrameLayout? = null, - private val timeReadContainer: View, - private val attachmentContainer: LinearLayoutCompat? = null, - - private val profiles: Map, - private val groups: Map, - - private val isForwards: Boolean = false -) { - - private val rootHighlightedColor = - ContextCompat.getColor(context, R.color.n2_100) - - private val mentionColor = - ContextCompat.getColor(context, R.color.colorPrimary) - - private var photoClickListener: ((url: String) -> Unit)? = null - private var replyClickListener: ((replyMessage: VkMessage) -> Unit)? = null - private var forwardsClickListener: ((forwards: List) -> Unit)? = null - - fun withPhotoClickListener(block: ((url: String) -> Unit)?): MessagesPreparator { - this.photoClickListener = block - return this - } - - fun withReplyClickListener(block: ((replyMessage: VkMessage) -> Unit)?): MessagesPreparator { - this.replyClickListener = block - return this - } - - fun withForwardsClickListener(block: ((forwards: List) -> Unit)?): MessagesPreparator { - this.forwardsClickListener = block - return this - } - - fun prepare() { - val messageUser = VkUtils.getMessageUser(message, profiles) - val messageGroup = VkUtils.getMessageGroup(message, groups) - - prepareRootBackground() - - prepareTime() - - prepareUnreadIndicator() - - prepareSpacer() - - prepareAttachments() - - prepareBubbleBackground() - - prepareText() - - prepareAvatar( - messageUser = messageUser, - messageGroup = messageGroup - ) - - if (message.isPeerChat() || isForwards) { - val prevSenderDiff = VkUtils.isPreviousMessageFromDifferentSender(prevMessage, message) - val nextSenderDiff = VkUtils.isPreviousMessageFromDifferentSender(message, nextMessage) - val fiveMinAgo = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message) - val nextMessageFiveMinAfter = - VkUtils.isPreviousMessageSentFiveMinutesAgo(message, nextMessage) - - val change = (prevMessage?.date ?: 0) - message.date - - Log.d( - "Fast::MessagesPreparator", - "text: ${message.text}; prevText: ${prevMessage?.text}; time change: $change; fromDiffSender: $prevSenderDiff; fiveMinAgo: $fiveMinAgo; " - ) - - title?.toggleVisibility(prevSenderDiff || fiveMinAgo) - - avatar?.visibility = - if (nextSenderDiff - || (fiveMinAgo && prevSenderDiff && nextMessageFiveMinAfter) - || nextMessageFiveMinAfter - || (!prevSenderDiff && nextSenderDiff) - || nextMessage == null - ) View.VISIBLE else View.INVISIBLE - } else { - title?.gone() - avatar?.gone() - } - - - bubble.run { - updateLayoutParams { - matchConstraintMaxWidth = - if (avatar?.isVisible == true) AppGlobal.screenWidth80 - avatar.width - 6.dpToPx() - else AppGlobal.screenWidth80 - } - } - - if (title != null) { - val titleString = when { - message.isUser() && messageUser != null -> messageUser.fullName - message.isGroup() && messageGroup != null -> messageGroup.name - else -> null - } - - title.text = titleString.orDots() - } - } - - private fun prepareRootBackground() { - if (root != null) { - root.background = - if (message.isSelected) ColorDrawable(rootHighlightedColor) - else null - } - } - - private fun prepareTime() { - val sentTime = SimpleDateFormat("HH:mm", Locale.getDefault()).format(message.date * 1000L) - - val timeText = - if (message.isUpdated()) context.getString(R.string.message_update_time_short, sentTime) - else sentTime - - time?.text = timeText - } - - private fun prepareUnreadIndicator() { - val isMessageRead = message.isRead(conversation) - - val drawableRes: Int = when (message.state) { - VkMessage.State.Sending -> { - R.drawable.ic_round_access_time_24 - } - VkMessage.State.Error -> { - R.drawable.ic_round_error_outline_24 - } - VkMessage.State.Sent -> { - if (isMessageRead) R.drawable.ic_round_done_all_24 - else R.drawable.ic_round_done_24 - } - } - - messageState?.run { - imageTintList = ColorStateList.valueOf( - ContextCompat.getColor( - context, - if (message.state == VkMessage.State.Error) R.color.colorError - else R.color.colorOnBackground - ) - ) - - toggleVisibility(!isMessageRead || message.isOut) - setImageResource(drawableRes) - } - } - - private fun prepareSpacer() { - val fiveMinAgo = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message) - val prevSenderDiff = VkUtils.isPreviousMessageFromDifferentSender(prevMessage, message) - spacer?.toggleVisibility(fiveMinAgo || prevSenderDiff) - } - - private fun prepareAttachments() { - attachmentContainer?.removeAllViews() - - if (attachmentContainer != null && replyContainer != null) { - if ( - !message.hasAttachments() && - !message.hasReply() && - !message.hasForwards() && - !message.hasGeo() - ) { - attachmentContainer.gone() - replyContainer.gone() - } else { - AttachmentInflater( - context = context, - container = attachmentContainer, - replyContainer = replyContainer, - timeReadContainer = timeReadContainer, - message = message, - groups = groups, - profiles = profiles - ) - .withPhotoClickListener(photoClickListener) - .withReplyClickListener(replyClickListener) - .withForwardsClickListener(forwardsClickListener) - .inflate() - } - } - } - - private fun prepareBubbleBackground() { -// bubble.background = if (message.isOut) backgroundMiddleOut else backgroundMiddleIn - } - - private fun prepareText() { - if (text != null) { - text.setOnClickListener { adapterClickListener?.invoke(position) } - text.movementMethod = LinkMovementMethod.getInstance() - text.updateLayoutParams { - val topMargin = (if (title != null && title.isVisible) 6 else 0).dpToPx() - - goneTopMargin = topMargin - } - - if (message.text == null) { - text.clear() - text.gone() - } else { - text.visible() - - val updSpacer = "\t\t\t\t" - val timeSpacer = "\t\t\t\t\t\t" - val messageStateSpacer = "\t\t\t" - - val preparedText = - VkUtils.prepareMessageText(message.text ?: "") + - (if (message.isUpdated()) updSpacer else "") + - timeSpacer + - (if (!message.isOut && message.isRead(conversation)) "" else messageStateSpacer) - - val visualizedText = - VkUtils.visualizeMentions( - preparedText, - mentionColor, - onMentionClick = { id -> - Toast.makeText(context, "id: $id", Toast.LENGTH_SHORT).show() - } - ) - - text.text = visualizedText - } - } - } - - private fun prepareAvatar( - messageUser: VkUser? = null, - messageGroup: VkGroup? = null - ) { - if (avatar != null) { - val avatarUrl = VkUtils.getMessageAvatar(message, messageUser, messageGroup) - - avatar.loadWithGlide { - imageUrl = avatarUrl - crossFade = true - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/di/MessagesHistoryModule.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/di/MessagesHistoryModule.kt deleted file mode 100644 index 17f1816a..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/di/MessagesHistoryModule.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.screens.messages.di - -import com.meloda.fast.screens.messages.MessagesHistoryViewModel -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.dsl.module - -val messagesHistoryModule = module { - viewModelOf(::MessagesHistoryViewModel) -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewFragment.kt deleted file mode 100644 index ca0f2051..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewFragment.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.meloda.fast.screens.photos - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import com.meloda.fast.base.BaseFragment -import org.koin.androidx.viewmodel.ext.android.viewModel - -class PhotoViewFragment : BaseFragment() { - - private val viewModel: PhotoViewViewModel by viewModel() - -// private val photosList: MutableList = mutableListOf() - - private var photoLink: String? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - photoLink = requireArguments().getString("photoLink") - -// val list: List<*>? = Gson().fromJson( -// requireArguments().getString("photosList"), -// List::class.java -// ) -// -// list?.forEach { if (it is VkPhoto) photosList.add(it) } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ImageView(requireContext()) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - photoLink?.let { viewModel.loadImageFromUrl(it, requireView() as ImageView) } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewViewModel.kt deleted file mode 100644 index 7eb6dc1b..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewViewModel.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.meloda.fast.screens.photos - -import android.widget.ImageView -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.meloda.fast.ext.ImageLoader.loadWithGlide -import kotlinx.coroutines.launch - -class PhotoViewViewModel : ViewModel() { - - fun loadImageFromUrl( - url: String, - imageView: ImageView, - ) = viewModelScope.launch { - imageView.loadWithGlide { imageUrl = url } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/photos/di/PhotoViewDI.kt b/app/src/main/kotlin/com/meloda/fast/screens/photos/di/PhotoViewDI.kt deleted file mode 100644 index 320f75ba..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/photos/di/PhotoViewDI.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.screens.photos.di - -import com.meloda.fast.screens.photos.PhotoViewViewModel -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.dsl.module - -val photoViewModule = module { - viewModelOf(::PhotoViewViewModel) -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/settings/SettingsFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/settings/SettingsFragment.kt deleted file mode 100644 index 116e99c1..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/settings/SettingsFragment.kt +++ /dev/null @@ -1,338 +0,0 @@ -package com.meloda.fast.screens.settings - -import android.annotation.SuppressLint -import android.app.StatusBarManager -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.os.Build -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.appcompat.app.AppCompatDelegate -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.res.painterResource -import androidx.core.os.bundleOf -import androidx.fragment.app.setFragmentResult -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig -import com.meloda.fast.base.BaseFragment -import com.meloda.fast.compose.MaterialDialog -import com.meloda.fast.ext.* -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.main.MainFragment -import com.meloda.fast.screens.main.activity.LongPollUtils -import com.meloda.fast.screens.main.activity.MainActivity -import com.meloda.fast.screens.settings.items.* -import com.meloda.fast.screens.settings.model.OnSettingsChangeListener -import com.meloda.fast.screens.settings.model.OnSettingsClickListener -import com.meloda.fast.screens.settings.model.OnSettingsLongClickListener -import com.meloda.fast.screens.settings.model.SettingsItem -import com.meloda.fast.screens.testing.TestActivity -import com.meloda.fast.service.LongPollQSTileService -import com.meloda.fast.ui.AppTheme -import kotlinx.coroutines.flow.update -import org.koin.androidx.viewmodel.ext.android.viewModel - -class SettingsFragment : BaseFragment() { - - private val viewModel: SettingsViewModel by viewModel() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ) = ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - listenViewModel() - - (view as? ComposeView)?.setContent { SettingsScreen() } - } - - private fun listenViewModel() { - viewModel.isLongPollBackgroundEnabled.listenValue(::handleLongPollEnabled) - viewModel.isNeedToOpenTestingActivity.listenValue(::handleOpenTestingActivity) - viewModel.isNeedToShowPerformCrashAlert.listenValue(::handlePerformCrashAlert) - viewModel.isNeedToShowAddQuickSettingsTileAlert.listenValue(::handleShowAddQuickSettingsTileAlert) - } - - private fun handleLongPollEnabled(newValue: Boolean?) { - if (newValue == null) return - - // TODO: 08.04.2023, Danil Nikolaev: rewrite this - LongPollUtils.requestNotificationsPermission( - fragmentActivity = requireActivity(), - onStateChangedAction = { newState -> MainActivity.longPollState.update { newState } }, - fromSettings = true - ) - } - - private fun handleOpenTestingActivity(newValue: Boolean) { - if (newValue) { - viewModel.onTestingActivityOpened() - context?.startActivity(Intent(context, TestActivity::class.java)) - } - } - - private fun handlePerformCrashAlert(newValue: Boolean) { - if (newValue) { - context?.showDialog( - title = UiText.Simple("Perform Crash"), - message = UiText.Simple("App will be crashed. Are you sure?"), - positiveText = UiText.Resource(R.string.yes), - positiveAction = viewModel::onPerformCrashPositiveButtonClicked, - negativeText = UiText.Resource(R.string.cancel), - onDismissAction = viewModel::onPerformCrashAlertDismissed - ) - } - } - - @SuppressLint("WrongConstant") - private fun handleShowAddQuickSettingsTileAlert(newValue: Boolean) { - if (newValue) { - viewModel.onAddQuickSettingsTileAlertShown() - - if (Build.VERSION.SDK_INT >= 33) { - val statusBarManager = - requireContext().getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager - statusBarManager.requestAddTileService( - ComponentName( - requireActivity(), LongPollQSTileService::class.java - ), - "Open Settings", - android.graphics.drawable.Icon.createWithResource( - requireActivity(), - R.drawable.ic_round_settings_24 - ), - {}, - {} - ) - } - } - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun SettingsScreen() { - val view = LocalView.current - - val useDynamicColors by viewModel.useDynamicColors.collectAsStateWithLifecycle() - val useLargeTopAppBar by viewModel.useLargeTopAppBar.collectAsStateWithLifecycle() - val isMultilineEnabled by viewModel.isMultilineEnabled.collectAsStateWithLifecycle() - val settings by viewModel.settings.collectAsStateWithLifecycle() - - val isNeedToShowLogOutDialog by viewModel.isNeedToShowLogOutAlert.collectAsStateWithLifecycle() - - val useHaptics by viewModel.isNeedToUseHaptics.collectAsStateWithLifecycle() - val hapticType = useHaptics.getHaptic() - view.performHapticFeedback(hapticType) - - HandleDialogs( - isNeedToShowLogOutDialog = isNeedToShowLogOutDialog - ) - - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( - rememberTopAppBarState() - ) - val scaffoldModifier = if (useLargeTopAppBar) { - Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - } else { - Modifier.fillMaxSize() - } - - val clickListener = OnSettingsClickListener(viewModel::onSettingsItemClicked) - val longClickListener = OnSettingsLongClickListener(viewModel::onSettingsItemLongClicked) - val changeListener = OnSettingsChangeListener(viewModel::onSettingsItemChanged) - - // TODO: 17.04.2023, Danil Nikolaev: make it work - val systemUiController = rememberSystemUiController() - DisposableEffect(systemUiController) { - systemUiController.systemBarsDarkContentEnabled = !isSystemUsingDarkMode() - onDispose {} - } - - AppTheme(useDynamicColors = useDynamicColors) { - Scaffold( - modifier = scaffoldModifier, - topBar = { - val title = @Composable { Text(text = "Settings") } - val navigationIcon = @Composable { - IconButton(onClick = { activity?.onBackPressedDispatcher?.onBackPressed() }) { - Icon( - painter = painterResource(id = R.drawable.ic_round_arrow_back_24), - contentDescription = null - ) - } - } - if (useLargeTopAppBar) { - LargeTopAppBar( - title = title, - navigationIcon = navigationIcon, - scrollBehavior = scrollBehavior - ) - } else { - TopAppBar( - title = title, - navigationIcon = navigationIcon - ) - } - } - ) { padding -> - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .padding(padding) - ) { - items( - count = settings.size, - key = { index -> - val item = settings[index] - (item.title ?: item.summary).notNull() - } - ) { index -> - when (val item = settings[index]) { - is SettingsItem.Title -> TitleSettingsItem( - item = item, - isMultiline = isMultilineEnabled - ) - - is SettingsItem.TitleSummary -> TitleSummarySettingsItem( - item = item, - isMultiline = isMultilineEnabled, - onSettingsClickListener = clickListener, - onSettingsLongClickListener = longClickListener - ) - - is SettingsItem.Switch -> SwitchSettingsItem( - item = item, - isMultiline = isMultilineEnabled, - onSettingsClickListener = clickListener, - onSettingsLongClickListener = longClickListener, - onSettingsChangeListener = changeListener - ) - - is SettingsItem.TextField -> EditTextSettingsItem( - item = item, - isMultiline = isMultilineEnabled, - onSettingsClickListener = clickListener, - onSettingsLongClickListener = longClickListener, - onSettingsChangeListener = changeListener - ) - - is SettingsItem.ListItem -> ListSettingsItem( - item = item, - isMultiline = isMultilineEnabled, - onSettingsClickListener = clickListener, - onSettingsLongClickListener = longClickListener, - onSettingsChangeListener = changeListener - ) - } - } - } - } - } - } - - @Composable - fun HandleDialogs( - isNeedToShowLogOutDialog: Boolean - ) { - if (isNeedToShowLogOutDialog) { - val isEasterEgg = UserConfig.userId == ID_DMITRY - - val title = UiText.Resource( - if (isEasterEgg) R.string.easter_egg_log_out_dmitry - else R.string.sign_out_confirm_title - ) - - val positiveText = UiText.Resource( - if (isEasterEgg) R.string.easter_egg_log_out_dmitry - else R.string.action_sign_out - ) - - MaterialDialog( - title = title, - message = UiText.Resource(R.string.sign_out_confirm), - positiveText = positiveText, - positiveAction = { - setFragmentResult( - MainFragment.START_SERVICES_KEY, - bundleOf(MainFragment.START_SERVICES_ARG_ENABLE to false) - ) - viewModel.onLogOutAlertPositiveClick() - }, - negativeText = UiText.Resource(R.string.cancel), - onDismissAction = viewModel::onLogOutAlertDismissed - ) - } - } - - companion object { - fun newInstance(): SettingsFragment = SettingsFragment() - - const val KEY_ACCOUNT = "account" - const val KEY_ACCOUNT_LOGOUT = "account_logout" - - const val KEY_APPEARANCE = "appearance" - const val KEY_APPEARANCE_MULTILINE = "appearance_multiline" - const val DEFAULT_VALUE_MULTILINE = true - - const val KEY_FEATURES_HIDE_KEYBOARD_ON_SCROLL = "features_hide_keyboard_on_scroll" - const val KEY_FEATURES_FAST_TEXT = "features_fast_text" - const val DEFAULT_VALUE_FEATURES_FAST_TEXT = "¯\\_(ツ)_/¯" - const val KEY_FEATURES_LONG_POLL_IN_BACKGROUND = "features_lp_background" - const val DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND = true - - const val KEY_VISIBILITY_SEND_ONLINE_STATUS = "visibility_send_online_status" - - const val KEY_UPDATES_CHECK_AT_STARTUP = "updates_check_at_startup" - const val KEY_UPDATES_CHECK_UPDATES = "updates_check_updates" - - const val KEY_MS_APPCENTER_ENABLE = "msappcenter.enable" - const val KEY_MS_APPCENTER_ENABLE_ON_DEBUG = "msappcenter.enable_on_debug" - - const val KEY_DEBUG_PERFORM_CRASH = "debug_perform_crash" - const val KEY_USE_DYNAMIC_COLORS = "debug_use_dynamic_colors" - const val DEFAULT_VALUE_USE_DYNAMIC_COLORS = false - const val KEY_DEBUG_SHOW_CRASH_ALERT = "debug_show_crash_alert" - const val KEY_APPEARANCE_DARK_THEME = "debug_appearance_dark_theme" - const val DEFAULT_VALUE_APPEARANCE_DARK_THEME = AppCompatDelegate.MODE_NIGHT_NO - const val KEY_USE_LARGE_TOP_APP_BAR = "debug_large_top_app_bar" - const val DEFAULT_VALUE_USE_LARGE_TOP_APP_BAR = true - const val KEY_OPEN_TESTING_ACTIVITY = "debug_open_testing_activity" - - const val KEY_DEBUG_HIDE_DEBUG_LIST = "debug_hide_debug_list" - - const val KEY_SHOW_DEBUG_CATEGORY = "show_debug_category" - - const val KEY_SHOW_EXACT_TIME_ON_TIME_STAMP = "show_exact_time_on_time_stamp" - - const val KEY_SHOW_ADD_QS_TILE_ALERT = "show_add_qs_tile_alert" - - - const val ID_DMITRY = 37610580 - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/settings/SettingsViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/settings/SettingsViewModel.kt deleted file mode 100644 index 4f6bb202..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/settings/SettingsViewModel.kt +++ /dev/null @@ -1,526 +0,0 @@ -package com.meloda.fast.screens.settings - -import android.os.Build -import android.view.HapticFeedbackConstants -import androidx.appcompat.app.AppCompatDelegate -import androidx.core.content.edit -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.github.terrakok.cicerone.Router -import com.meloda.fast.BuildConfig -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.common.Screens -import com.meloda.fast.data.account.AccountsDao -import com.meloda.fast.database.CacheDatabase -import com.meloda.fast.ext.emitOnMainScope -import com.meloda.fast.ext.ifEmpty -import com.meloda.fast.ext.isSdkAtLeast -import com.meloda.fast.ext.isTrue -import com.meloda.fast.model.base.UiText -import com.meloda.fast.model.base.parseString -import com.meloda.fast.screens.main.activity.LongPollState -import com.meloda.fast.screens.main.activity.MainActivity -import com.meloda.fast.screens.settings.model.SettingsItem -import com.microsoft.appcenter.crashes.model.TestCrashException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -typealias SettingsList = List> - -interface SettingsViewModel { - - val settings: StateFlow - val useDynamicColors: StateFlow - val useLargeTopAppBar: StateFlow - val isMultilineEnabled: StateFlow - val isLongPollBackgroundEnabled: StateFlow - - val isNeedToShowLogOutAlert: StateFlow - - val isNeedToOpenTestingActivity: StateFlow - - val isNeedToShowPerformCrashAlert: StateFlow - - val isNeedToShowAddQuickSettingsTileAlert: StateFlow - - val isNeedToUseHaptics: StateFlow - - fun onLogOutAlertDismissed() - - fun onPerformCrashAlertDismissed() - - fun onPerformCrashPositiveButtonClicked() - - fun onLogOutAlertPositiveClick() - - fun onSettingsItemClicked(key: String) - fun onSettingsItemLongClicked(key: String): Boolean - fun onSettingsItemChanged(key: String, newValue: Any?) - - fun onTestingActivityOpened() - - fun onAddQuickSettingsTileAlertShown() - - fun onHapticsUsed() -} - -class SettingsViewModelImpl constructor( - private val accountsDao: AccountsDao, - private val cacheDatabase: CacheDatabase, - private val router: Router -) : SettingsViewModel, ViewModel() { - - override val settings = MutableStateFlow(emptyList()) - override val useDynamicColors = MutableStateFlow( - AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_USE_DYNAMIC_COLORS, - SettingsFragment.DEFAULT_VALUE_USE_DYNAMIC_COLORS - ) - ) - override val useLargeTopAppBar = MutableStateFlow( - AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_USE_LARGE_TOP_APP_BAR, - SettingsFragment.DEFAULT_VALUE_USE_LARGE_TOP_APP_BAR - ) - ) - override val isMultilineEnabled = MutableStateFlow( - AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_APPEARANCE_MULTILINE, - SettingsFragment.DEFAULT_VALUE_MULTILINE - ) - ) - override val isLongPollBackgroundEnabled = MutableStateFlow(null) - - override val isNeedToShowLogOutAlert = MutableStateFlow(false) - - override val isNeedToOpenTestingActivity = MutableStateFlow(false) - - override val isNeedToShowPerformCrashAlert = MutableStateFlow(false) - - override val isNeedToShowAddQuickSettingsTileAlert = MutableStateFlow(false) - - override val isNeedToUseHaptics = MutableStateFlow(HapticType.None) - - init { - if (AppGlobal.preferences.getBoolean("first_open_settings", true)) { - AppGlobal.preferences.edit { - putBoolean("first_open_settings", false) - } - - isNeedToShowAddQuickSettingsTileAlert.emitOnMainScope(true) - } - - createSettings() - } - - private fun createSettings() { - viewModelScope.launch { - val accountVisible = UserConfig.isLoggedIn() - val accountTitle = SettingsItem.Title.build( - key = SettingsFragment.KEY_ACCOUNT, - title = UiText.Simple("Account") - ) { - isVisible = accountVisible - } - val accountLogOut = SettingsItem.TitleSummary.build( - key = SettingsFragment.KEY_ACCOUNT_LOGOUT, - title = UiText.Simple("Log out"), - summary = UiText.Simple("Log out from account and delete all local data related to this account") - ) { - isVisible = accountVisible - } - - val appearanceTitle = SettingsItem.Title.build( - key = SettingsFragment.KEY_APPEARANCE, - title = UiText.Simple("Appearance") - ) - val appearanceMultiline = SettingsItem.Switch.build( - key = SettingsFragment.KEY_APPEARANCE_MULTILINE, - defaultValue = SettingsFragment.DEFAULT_VALUE_MULTILINE, - title = UiText.Simple("Multiline titles and messages"), - summary = UiText.Simple("The title of the dialog and the text of the message can take up two lines") - ) - - val featuresTitle = SettingsItem.Title.build( - key = "features", - title = UiText.Simple("Features") - ) - val featuresHideKeyboardOnScroll = SettingsItem.Switch.build( - key = SettingsFragment.KEY_FEATURES_HIDE_KEYBOARD_ON_SCROLL, - defaultValue = true, - title = UiText.Simple("Hide keyboard on scroll"), - summary = UiText.Simple("Hides keyboard when you scrolling messages up in messages history screen") - ) - val featuresFastText = SettingsItem.TextField.build( - key = SettingsFragment.KEY_FEATURES_FAST_TEXT, - title = UiText.Simple("Fast text"), - defaultValue = "¯\\_(ツ)_/¯", - ).apply { - summaryProvider = SettingsItem.SummaryProvider { settingsItem -> - UiText.ResourceParams( - R.string.pref_message_fast_text_summary, - listOf(settingsItem.value.ifEmpty { null }) - ) - } - } - val featuresLongPollBackground = SettingsItem.Switch.build( - key = SettingsFragment.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, - defaultValue = SettingsFragment.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND, - title = UiText.Simple("LongPoll in background"), - summary = UiText.Simple("Your messages will be updates even when app is not on the screen") - ) - - val visibilityTitle = SettingsItem.Title.build( - key = "visibility", - title = UiText.Simple("Visibility") - ) - val visibilitySendOnlineStatus = SettingsItem.Switch.build( - key = SettingsFragment.KEY_VISIBILITY_SEND_ONLINE_STATUS, - defaultValue = false, - title = UiText.Simple("Send online status"), - summary = UiText.Simple("Online status will be sent every five minutes") - ) - - val updatesTitle = SettingsItem.Title.build( - key = "updates", - title = UiText.Simple("Updates") - ) - val updatesCheckAtStartup = SettingsItem.Switch.build( - key = SettingsFragment.KEY_UPDATES_CHECK_AT_STARTUP, - title = UiText.Simple("Check at startup"), - summary = UiText.Simple("Check updates at app startup"), - defaultValue = true - ) - val updatesCheckUpdates = SettingsItem.TitleSummary.build( - key = SettingsFragment.KEY_UPDATES_CHECK_UPDATES, - title = UiText.Simple("Check updates") - ) - - val msAppCenterTitle = SettingsItem.Title.build( - key = "msappcenter", - title = UiText.Simple("MS AppCenter Crash Reporter") - ) - val msAppCenterEnable = SettingsItem.Switch.build( - key = SettingsFragment.KEY_MS_APPCENTER_ENABLE, - defaultValue = true, - title = UiText.Simple("Enable Crash Reporter") - ) - val msAppCenterEnableOnDebug = SettingsItem.Switch.build( - key = SettingsFragment.KEY_MS_APPCENTER_ENABLE_ON_DEBUG, - defaultValue = false, - title = UiText.Simple("Enable Crash Reporter on debug builds"), - summary = UiText.Simple("Requires application restart") - ) - - val debugTitle = SettingsItem.Title.build( - key = "debug", - title = UiText.Simple("Debug") - ) - val debugPerformCrash = SettingsItem.TitleSummary.build( - key = SettingsFragment.KEY_DEBUG_PERFORM_CRASH, - title = UiText.Simple("Perform crash"), - summary = UiText.Simple("App will be crashed. Obviously") - ) - val debugShowCrashAlert = SettingsItem.Switch.build( - key = SettingsFragment.KEY_DEBUG_SHOW_CRASH_ALERT, - defaultValue = true, - title = UiText.Simple("Show alert after crash"), - summary = UiText.Simple("Shows alert dialog with stacktrace after app crashed\n(it will be not shown if you perform crash manually)") - ) - val debugUseDynamicColors = SettingsItem.Switch.build( - key = SettingsFragment.KEY_USE_DYNAMIC_COLORS, - title = UiText.Simple("[WIP] Use dynamic colors"), - isEnabled = isSdkAtLeast(Build.VERSION_CODES.S), - summary = UiText.Simple("Requires Android 12 or higher;\nUnstable - you may need to manually kill app via it's info screen in order for changes to applied"), - defaultValue = false - ) - - val darkThemeValues = listOf( - AppCompatDelegate.MODE_NIGHT_YES, - AppCompatDelegate.MODE_NIGHT_NO, - AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, - AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY - ) - val darkThemeTitles = listOf( - UiText.Simple("Enabled"), - UiText.Simple("Disabled"), - UiText.Simple("Follow system"), - UiText.Simple("Battery saver") - ) - val darkThemeValuesMap = List(darkThemeValues.size) { index -> - darkThemeValues[index] to darkThemeTitles[index].parseString(AppGlobal.Instance) - }.toMap() - - val debugDarkTheme = SettingsItem.ListItem.build( - key = SettingsFragment.KEY_APPEARANCE_DARK_THEME, - title = UiText.Simple("[WIP] Dark theme"), - values = darkThemeValues, - valueTitles = darkThemeTitles, - defaultValue = AppCompatDelegate.MODE_NIGHT_NO - ) { - summaryProvider = SettingsItem.SummaryProvider { item -> - UiText.Simple( - "Current value: ${ - darkThemeValuesMap.getOrElse(item.value ?: -1) { - "Unknown" - } - }" - ) - } - } - val debugUseLargeTopAppBar = SettingsItem.Switch.build( - key = SettingsFragment.KEY_USE_LARGE_TOP_APP_BAR, - title = UiText.Simple("[WIP] Use LargeTopAppBar"), - summary = UiText.Simple("Using large top appbar instead of default toolbar everywhere in app"), - defaultValue = SettingsFragment.DEFAULT_VALUE_USE_LARGE_TOP_APP_BAR - ) - val debugOpenTestingActivity = SettingsItem.TitleSummary.build( - key = SettingsFragment.KEY_OPEN_TESTING_ACTIVITY, - title = UiText.Simple("Open testing activity") - ) - val debugShowExactTimeOnTimeStamp = SettingsItem.Switch.build( - key = SettingsFragment.KEY_SHOW_EXACT_TIME_ON_TIME_STAMP, - title = UiText.Simple("Show exact time on time stamp"), - summary = UiText.Simple("Shows hours and minutes on time stamp in messages history"), - defaultValue = false - ) - val debugShowAddQuickSettingsTileAlert = SettingsItem.TitleSummary.build( - key = SettingsFragment.KEY_SHOW_ADD_QS_TILE_ALERT, - title = UiText.Simple("Add QuickSettings Tile") - ) - - val debugHideDebugList = SettingsItem.TitleSummary.build( - key = SettingsFragment.KEY_DEBUG_HIDE_DEBUG_LIST, - title = UiText.Simple("Hide debug list") - ) - - val accountList = listOf( - accountTitle, - accountLogOut - ) - val appearanceList = listOf( - appearanceTitle, - appearanceMultiline - ) - val featuresList = listOf( - featuresTitle, - featuresHideKeyboardOnScroll, - featuresFastText, - featuresLongPollBackground - ) - val visibilityList = listOf( - visibilityTitle, - visibilitySendOnlineStatus, - ) - val updatesList = listOf( - updatesTitle, - updatesCheckAtStartup, - updatesCheckUpdates, - ) - val msAppCenterList = mutableListOf( - msAppCenterTitle, - msAppCenterEnable, - ).apply { - if (BuildConfig.DEBUG) { - this += msAppCenterEnableOnDebug - } - } - val debugList = mutableListOf>() - listOf( - debugTitle, - debugPerformCrash, - debugShowCrashAlert, - debugUseDynamicColors, - debugDarkTheme, - debugUseLargeTopAppBar, - debugOpenTestingActivity, - debugShowExactTimeOnTimeStamp, - debugShowAddQuickSettingsTileAlert - ).forEach(debugList::add) - - debugList += debugHideDebugList - - val settingsList = mutableListOf>() - listOf( - accountList, - appearanceList, - featuresList, - visibilityList, - updatesList, - msAppCenterList, - debugList, - ).forEach(settingsList::addAll) - - if (!AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_SHOW_DEBUG_CATEGORY, - false - ) - ) { - settingsList.removeAll(debugList) - } - - settings.emit(settingsList) - } - } - - override fun onLogOutAlertDismissed() { - viewModelScope.launch(Dispatchers.Main) { - isNeedToShowLogOutAlert.emit(false) - } - } - - override fun onPerformCrashAlertDismissed() { - isNeedToShowPerformCrashAlert.emitOnMainScope(false) - } - - override fun onPerformCrashPositiveButtonClicked() { - isNeedToShowPerformCrashAlert.emitOnMainScope(false) - throw TestCrashException() - } - - override fun onLogOutAlertPositiveClick() { - viewModelScope.launch(Dispatchers.IO) { - accountsDao.deleteById(UserConfig.userId) - cacheDatabase.clearAllTables() - - MainActivity.longPollState.emit(LongPollState.Stop) - - UserConfig.clear() - - withContext(Dispatchers.Main) { - router.newRootScreen(Screens.Main()) - } - } - } - - override fun onSettingsItemClicked(key: String) { - when (key) { - SettingsFragment.KEY_ACCOUNT_LOGOUT -> { - viewModelScope.launch(Dispatchers.Main) { - isNeedToShowLogOutAlert.emit(true) - } - } - - SettingsFragment.KEY_UPDATES_CHECK_UPDATES -> { - openUpdatesScreen() - } - - SettingsFragment.KEY_DEBUG_PERFORM_CRASH -> { - isNeedToShowPerformCrashAlert.emitOnMainScope(true) - } - - SettingsFragment.KEY_DEBUG_HIDE_DEBUG_LIST -> { - val showDebugCategory = - AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_SHOW_DEBUG_CATEGORY, - false - ) - if (!showDebugCategory) return - - AppGlobal.preferences.edit { - putBoolean(SettingsFragment.KEY_SHOW_DEBUG_CATEGORY, false) - } - - createSettings() - - isNeedToUseHaptics.emitOnMainScope(HapticType.HideDebugMenu) - } - - SettingsFragment.KEY_OPEN_TESTING_ACTIVITY -> { - isNeedToOpenTestingActivity.emitOnMainScope(true) - } - - SettingsFragment.KEY_SHOW_ADD_QS_TILE_ALERT -> { - isNeedToShowAddQuickSettingsTileAlert.emitOnMainScope(true) - } - } - } - - override fun onSettingsItemLongClicked(key: String): Boolean { - return when (key) { - SettingsFragment.KEY_UPDATES_CHECK_UPDATES -> { - val showDebugCategory = - AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_SHOW_DEBUG_CATEGORY, - false - ) - if (showDebugCategory) return false - - AppGlobal.preferences.edit { - putBoolean(SettingsFragment.KEY_SHOW_DEBUG_CATEGORY, true) - } - createSettings() - - isNeedToUseHaptics.emitOnMainScope(HapticType.ShowDebugMenu) - true - } - - else -> false - } - } - - override fun onSettingsItemChanged(key: String, newValue: Any?) { - when (key) { - SettingsFragment.KEY_APPEARANCE_DARK_THEME -> { - val newMode = newValue as? Int ?: return - AppCompatDelegate.setDefaultNightMode(newMode) - } - - SettingsFragment.KEY_APPEARANCE_MULTILINE -> { - val isEnabled = (newValue as? Boolean).isTrue - isMultilineEnabled.update { isEnabled } - } - - SettingsFragment.KEY_USE_DYNAMIC_COLORS -> { - val isEnabled = (newValue as? Boolean).isTrue - useDynamicColors.update { isEnabled } - } - - SettingsFragment.KEY_USE_LARGE_TOP_APP_BAR -> { - val isEnabled = (newValue as? Boolean).isTrue - useLargeTopAppBar.update { isEnabled } - } - - SettingsFragment.KEY_FEATURES_LONG_POLL_IN_BACKGROUND -> { - val isEnabled = (newValue as? Boolean).isTrue - isLongPollBackgroundEnabled.update { isEnabled } - } - } - } - - override fun onTestingActivityOpened() { - isNeedToOpenTestingActivity.emitOnMainScope(false) - } - - override fun onAddQuickSettingsTileAlertShown() { - isNeedToShowAddQuickSettingsTileAlert.emitOnMainScope(false) - } - - override fun onHapticsUsed() { - isNeedToUseHaptics.emitOnMainScope(HapticType.None) - } - - private fun openUpdatesScreen() { - router.navigateTo(Screens.Updates()) - } -} - -sealed interface HapticType { - object ShowDebugMenu : HapticType - object HideDebugMenu : HapticType - object None : HapticType - - fun getHaptic(): Int { - return when (this) { - ShowDebugMenu -> HapticFeedbackConstants.LONG_PRESS - HideDebugMenu -> HapticFeedbackConstants.REJECT - None -> -1 - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/settings/di/SettingsModule.kt b/app/src/main/kotlin/com/meloda/fast/screens/settings/di/SettingsModule.kt deleted file mode 100644 index f9dd86c6..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/settings/di/SettingsModule.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.screens.settings.di - -import com.meloda.fast.screens.settings.SettingsViewModelImpl -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.dsl.module - -val settingsModule = module { - viewModelOf(::SettingsViewModelImpl) -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/settings/items/ListSettingsItem.kt b/app/src/main/kotlin/com/meloda/fast/screens/settings/items/ListSettingsItem.kt deleted file mode 100644 index c742e7c7..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/settings/items/ListSettingsItem.kt +++ /dev/null @@ -1,122 +0,0 @@ -package com.meloda.fast.screens.settings.items - -import android.content.Context -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.* -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import com.meloda.fast.R -import com.meloda.fast.ext.ItemsChoiceType -import com.meloda.fast.ext.getString -import com.meloda.fast.ext.showDialog -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.settings.model.OnSettingsChangeListener -import com.meloda.fast.screens.settings.model.OnSettingsClickListener -import com.meloda.fast.screens.settings.model.OnSettingsLongClickListener -import com.meloda.fast.screens.settings.model.SettingsItem - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun ListSettingsItem( - item: SettingsItem.ListItem, - isMultiline: Boolean, - onSettingsClickListener: OnSettingsClickListener, - onSettingsLongClickListener: OnSettingsLongClickListener, - onSettingsChangeListener: OnSettingsChangeListener -) { - val context = LocalContext.current - - var title by remember { mutableStateOf(item.title) } - item.onTitleChanged = { newTitle -> title = newTitle } - - var summary by remember { mutableStateOf(item.summary) } - item.onSummaryChanged = { newSummary -> summary = newSummary } - - var isEnabled by remember { mutableStateOf(item.isEnabled) } - item.onEnabledStateChanged = { newEnabled -> isEnabled = newEnabled } - - var isVisible by remember { mutableStateOf(item.isVisible) } - item.onVisibleStateChanged = { newVisible -> isVisible = newVisible } - - if (!isVisible) return - Row( - modifier = Modifier - .heightIn(min = 56.dp) - .fillMaxWidth() - .combinedClickable( - enabled = isEnabled, - onClick = { - onSettingsClickListener.onClick(item.key) - showListAlertDialog( - context = context, - item = item, - onSettingsChangeListener = { key, newValue -> - summary = item.summaryProvider?.provideSummary(item) - onSettingsChangeListener.onChange(key, newValue) - } - ) - }, - onLongClick = { onSettingsLongClickListener.onLongClick(item.key) }, - ) - ) { - Spacer(modifier = Modifier.width(16.dp)) - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.Start - ) { - Spacer(modifier = Modifier.height(14.dp)) - title?.getString()?.let { title -> - Text( - text = title, - style = MaterialTheme.typography.headlineSmall, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, - overflow = TextOverflow.Ellipsis, - ) - } - summary?.getString()?.let { summary -> - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, - overflow = TextOverflow.Ellipsis, - ) - } - Spacer(modifier = Modifier.height(14.dp)) - } - Spacer(modifier = Modifier.width(16.dp)) - } -} - -private fun showListAlertDialog( - context: Context, - item: SettingsItem.ListItem, - onSettingsChangeListener: OnSettingsChangeListener -) { - var selectedOption = item.value - val checkedItem = item.values.indexOf(selectedOption) - - context.showDialog( - title = item.title, - items = item.valueTitles, - checkedItems = listOf(checkedItem), - itemsChoiceType = ItemsChoiceType.SingleChoice, - itemsClickAction = { index, _ -> - selectedOption = item.values[index] - }, - positiveText = UiText.Resource(R.string.ok), - positiveAction = { - if (item.value != selectedOption) { - item.value = selectedOption - onSettingsChangeListener.onChange(item.key, selectedOption) - } - } - ) -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/settings/items/TextFieldSettingsItem.kt b/app/src/main/kotlin/com/meloda/fast/screens/settings/items/TextFieldSettingsItem.kt deleted file mode 100644 index 1639e0e9..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/settings/items/TextFieldSettingsItem.kt +++ /dev/null @@ -1,187 +0,0 @@ -package com.meloda.fast.screens.settings.items - -import android.content.Context -import android.view.LayoutInflater -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import com.meloda.fast.R -import com.meloda.fast.compose.MaterialDialog -import com.meloda.fast.databinding.ItemSettingsEditTextAlertBinding -import com.meloda.fast.ext.getString -import com.meloda.fast.ext.showDialog -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.settings.model.OnSettingsChangeListener -import com.meloda.fast.screens.settings.model.OnSettingsClickListener -import com.meloda.fast.screens.settings.model.OnSettingsLongClickListener -import com.meloda.fast.screens.settings.model.SettingsItem - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun EditTextSettingsItem( - item: SettingsItem.TextField, - isMultiline: Boolean, - onSettingsClickListener: OnSettingsClickListener, - onSettingsLongClickListener: OnSettingsLongClickListener, - onSettingsChangeListener: OnSettingsChangeListener -) { - val context = LocalContext.current - - var title by remember { mutableStateOf(item.title) } - item.onTitleChanged = { newTitle -> title = newTitle } - - var summary by remember { mutableStateOf(item.summary) } - item.onSummaryChanged = { newSummary -> summary = newSummary } - - // TODO: 07.04.2023, Danil Nikolaev: handle isEnabled - var isEnabled by remember { mutableStateOf(item.isEnabled) } - item.onEnabledStateChanged = { newEnabled -> isEnabled = newEnabled } - - var isVisible by remember { mutableStateOf(item.isVisible) } - item.onVisibleStateChanged = { newVisible -> isVisible = newVisible } - - var showDialog by remember { - mutableStateOf(false) - } - - if (showDialog) { - ShowEditTextAlert( - item = item, - onSettingsChangeListener = { key, newValue -> - summary = item.summaryProvider?.provideSummary(item) - onSettingsChangeListener.onChange(key, newValue) - }, - onDismiss = { showDialog = false } - ) - } - - if (!isVisible) return - Row( - modifier = Modifier - .heightIn(min = 56.dp) - .fillMaxWidth() - .combinedClickable( - enabled = isEnabled, - onClick = { - onSettingsClickListener.onClick(item.key) - showDialog = true - }, - onLongClick = { onSettingsLongClickListener.onLongClick(item.key) }, - ) - ) { - Spacer(modifier = Modifier.width(16.dp)) - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.Start - ) { - Spacer(modifier = Modifier.height(14.dp)) - title?.getString()?.let { title -> - Text( - text = title, - style = MaterialTheme.typography.headlineSmall, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, - overflow = TextOverflow.Ellipsis, - ) - } - summary?.getString()?.let { summary -> - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, - overflow = TextOverflow.Ellipsis, - ) - } - Spacer(modifier = Modifier.height(14.dp)) - } - Spacer(modifier = Modifier.width(16.dp)) - } -} - -private fun showEditTextAlert( - context: Context, - item: SettingsItem.TextField, - onSettingsChangeListener: OnSettingsChangeListener -) { - val binding = ItemSettingsEditTextAlertBinding.inflate( - LayoutInflater.from(context), null, false - ) - - binding.editText.setText(item.value) - - context.showDialog( - title = item.title, - view = binding.root, - positiveText = UiText.Resource(R.string.ok), - positiveAction = { - val newValue = binding.editText.text.toString() - - if (item.value != newValue) { - item.value = newValue - onSettingsChangeListener.onChange(item.key, newValue) - } - }, - negativeText = UiText.Resource(R.string.cancel) - ) -} - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) -@Composable -fun ShowEditTextAlert( - item: SettingsItem.TextField, - onSettingsChangeListener: OnSettingsChangeListener, - onDismiss: () -> Unit -) { - val (textFieldFocusable) = FocusRequester.createRefs() - - var textFieldValue by remember { - mutableStateOf(TextFieldValue(item.value.orEmpty())) - } - - MaterialDialog( - title = item.title, - positiveText = UiText.Resource(R.string.ok), - positiveAction = { - val newValue = textFieldValue.text.trim() - - if (item.value != newValue) { - item.value = newValue - onSettingsChangeListener.onChange(item.key, newValue) - } - }, - negativeText = UiText.Resource(R.string.cancel), - onDismissAction = onDismiss - ) { - TextField( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)) - .focusRequester(textFieldFocusable), - value = textFieldValue, - onValueChange = { newText -> - textFieldValue = newText - }, - label = { Text(text = "Value") }, - placeholder = { Text(text = "Value") }, - shape = RoundedCornerShape(10.dp), - ) - } - - LaunchedEffect(Unit) { - textFieldFocusable.requestFocus() - textFieldValue = textFieldValue.copy(selection = TextRange(textFieldValue.text.length)) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/settings/items/TitleSummarySettingsItem.kt b/app/src/main/kotlin/com/meloda/fast/screens/settings/items/TitleSummarySettingsItem.kt deleted file mode 100644 index 89642d38..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/settings/items/TitleSummarySettingsItem.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.meloda.fast.screens.settings.items - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.* -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import com.meloda.fast.ext.getString -import com.meloda.fast.screens.settings.model.OnSettingsClickListener -import com.meloda.fast.screens.settings.model.OnSettingsLongClickListener -import com.meloda.fast.screens.settings.model.SettingsItem - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun TitleSummarySettingsItem( - item: SettingsItem.TitleSummary, - isMultiline: Boolean, - onSettingsClickListener: OnSettingsClickListener, - onSettingsLongClickListener: OnSettingsLongClickListener -) { - var title by remember { mutableStateOf(item.title) } - item.onTitleChanged = { newTitle -> title = newTitle } - - var summary by remember { mutableStateOf(item.summary) } - item.onSummaryChanged = { newSummary -> summary = newSummary } - - // TODO: 08.04.2023, Danil Nikolaev: handle isEnabled state - var isEnabled by remember { mutableStateOf(item.isEnabled) } - item.onEnabledStateChanged = { newEnabled -> isEnabled = newEnabled } - - var isVisible by remember { mutableStateOf(item.isVisible) } - item.onVisibleStateChanged = { newVisible -> isVisible = newVisible } - - if (!isVisible) return - Row( - modifier = Modifier - .heightIn(min = 56.dp) - .fillMaxWidth() - .combinedClickable( - enabled = isEnabled, - onClick = { onSettingsClickListener.onClick(item.key) }, - onLongClick = { onSettingsLongClickListener.onLongClick(item.key) }, - ) - ) { - Spacer(modifier = Modifier.width(16.dp)) - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.Start - ) { - Spacer(modifier = Modifier.height(14.dp)) - title?.getString()?.let { title -> - Text( - text = title, - style = MaterialTheme.typography.headlineSmall, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, - overflow = TextOverflow.Ellipsis, - ) - } - summary?.getString()?.let { summary -> - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium, - maxLines = if (isMultiline) Int.MAX_VALUE else 1, - overflow = TextOverflow.Ellipsis, - ) - } - Spacer(modifier = Modifier.height(14.dp)) - } - Spacer(modifier = Modifier.width(16.dp)) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/testing/TestActivity.kt b/app/src/main/kotlin/com/meloda/fast/screens/testing/TestActivity.kt deleted file mode 100644 index 147fabd2..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/testing/TestActivity.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.meloda.fast.screens.testing - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.ext.edgeToEdge -import com.meloda.fast.screens.settings.SettingsFragment -import com.meloda.fast.ui.AppTheme - -class TestActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - edgeToEdge() - - setContent { - TestingScreen() - } - } - - @Preview - @Composable - fun TestingScreenPreview() { - TestingScreen() - } - - @Composable - fun TestingScreen() { - val useDynamicColors = AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_USE_DYNAMIC_COLORS, - SettingsFragment.DEFAULT_VALUE_USE_DYNAMIC_COLORS - ) - AppTheme(useDynamicColors = useDynamicColors) { - Scaffold(modifier = Modifier.fillMaxSize()) { padding -> - Surface( - modifier = Modifier - .fillMaxSize() - .padding(padding) - ) { - Column { - Button(onClick = {}) { - Text(text = "Button") - } - Text(text = "Testing text") - } - } - } - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/TwoFaScreens.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/TwoFaScreens.kt deleted file mode 100644 index 242c1b77..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/TwoFaScreens.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.meloda.fast.screens.twofa - -import com.github.terrakok.cicerone.androidx.FragmentScreen -import com.meloda.fast.screens.twofa.presentation.TwoFaFragment - -object TwoFaScreens { - - fun twoFaScreen() = FragmentScreen(key = "TwoFaScreen") { - TwoFaFragment.newInstance() - } - -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/di/TwoFaDI.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/di/TwoFaDI.kt deleted file mode 100644 index 62eec829..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/di/TwoFaDI.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.meloda.fast.screens.twofa.di - -import com.meloda.fast.di.navigationModule -import com.meloda.fast.screens.twofa.presentation.TwoFaViewModelImpl -import com.meloda.fast.screens.twofa.screen.TwoFaCoordinator -import com.meloda.fast.screens.twofa.screen.TwoFaCoordinatorImpl -import com.meloda.fast.screens.twofa.screen.TwoFaScreen -import com.meloda.fast.screens.twofa.validation.TwoFaValidator -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.core.module.dsl.singleOf -import org.koin.core.qualifier.named -import org.koin.core.scope.Scope -import org.koin.dsl.bind -import org.koin.dsl.module - -val twoFaModule = module { - val moduleQualifier = named("twoFa") - - includes(navigationModule) - - single(moduleQualifier) { screen().resultFlow } - single { screen().getArguments() } - - single { - TwoFaCoordinatorImpl( - resultFlow = get(moduleQualifier), - router = get() - ) - } bind TwoFaCoordinator::class - - singleOf(::TwoFaValidator) - viewModelOf(::TwoFaViewModelImpl) -} - -private fun Scope.screen(): TwoFaScreen = get() diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaArguments.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaArguments.kt deleted file mode 100644 index f0b71e42..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaArguments.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.meloda.fast.screens.twofa.model - -import com.meloda.fast.model.base.UiText - -data class TwoFaArguments( - val validationSid: String, - val redirectUri: String, - val phoneMask: String, - val validationType: TwoFaValidationType, - val canResendSms: Boolean, - val wrongCodeError: UiText?, -) diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaResult.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaResult.kt deleted file mode 100644 index 2e7d7c3e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaResult.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.meloda.fast.screens.twofa.model - -sealed class TwoFaResult { - object Cancelled : TwoFaResult() - data class Success(val sid: String, val code: String) : TwoFaResult() -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaScreenState.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaScreenState.kt deleted file mode 100644 index fe9c518c..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaScreenState.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.meloda.fast.screens.twofa.model - -import com.meloda.fast.model.base.UiText - -data class TwoFaScreenState( - val twoFaSid: String, - val twoFaCode: String, - val twoFaText: UiText, - val canResendSms: Boolean, - val codeError: UiText?, - val delayTime: Int -) { - - companion object { - val EMPTY = TwoFaScreenState( - twoFaSid = "", - twoFaCode = "", - twoFaText = UiText.Simple(""), - canResendSms = false, - codeError = null, - delayTime = 0 - ) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaValidationResult.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaValidationResult.kt deleted file mode 100644 index 188dbc99..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/model/TwoFaValidationResult.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.meloda.fast.screens.twofa.model - -sealed class TwoFaValidationResult { - object Empty : TwoFaValidationResult() - object Valid : TwoFaValidationResult() - - fun isValid() = this == Valid -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/presentation/TwoFaViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/presentation/TwoFaViewModel.kt deleted file mode 100644 index eddb0ca5..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/presentation/TwoFaViewModel.kt +++ /dev/null @@ -1,172 +0,0 @@ -package com.meloda.fast.screens.twofa.presentation - -import androidx.lifecycle.viewModelScope -import com.meloda.fast.base.viewmodel.BaseViewModel -import com.meloda.fast.data.auth.AuthRepository -import com.meloda.fast.ext.createTimerFlow -import com.meloda.fast.ext.isTrue -import com.meloda.fast.ext.updateValue -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.twofa.model.TwoFaArguments -import com.meloda.fast.screens.twofa.model.TwoFaResult -import com.meloda.fast.screens.twofa.model.TwoFaScreenState -import com.meloda.fast.screens.twofa.model.TwoFaValidationType -import com.meloda.fast.screens.twofa.screen.TwoFaCoordinator -import com.meloda.fast.screens.twofa.validation.TwoFaValidator -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.launch - - -interface TwoFaViewModel { - - val screenState: StateFlow - - fun onCodeInputChanged(newCode: String) - - fun onBackButtonClicked() - fun onCancelButtonClicked() - fun onRequestSmsButtonClicked() - fun onTextFieldDoneClicked() - fun onDoneButtonClicked() -} - -class TwoFaViewModelImpl constructor( - private val coordinator: TwoFaCoordinator, - private val validator: TwoFaValidator, - private val authRepository: AuthRepository, - arguments: TwoFaArguments, -) : TwoFaViewModel, BaseViewModel() { - - override val screenState = MutableStateFlow(TwoFaScreenState.EMPTY) - - private var delayJob: Job? = null - - init { - if (arguments.wrongCodeError != null) { - screenState.updateValue( - screenState.value.copy(codeError = arguments.wrongCodeError) - ) - } - - screenState.updateValue( - screenState.value.copy( - twoFaSid = arguments.validationSid, - twoFaText = getTwoFaText(arguments.validationType), - canResendSms = arguments.canResendSms - ) - ) - } - - override fun onCodeInputChanged(newCode: String) { - screenState.updateValue( - screenState.value.copy( - twoFaCode = newCode.trim(), - codeError = null - ) - ) - - if (newCode.length == 6) { - onDoneButtonClicked() - } - } - - override fun onBackButtonClicked() { - onCancelButtonClicked() - } - - override fun onCancelButtonClicked() { - coordinator.finishWithResult(TwoFaResult.Cancelled) - } - - override fun onRequestSmsButtonClicked() { - sendValidationCode() - } - - override fun onTextFieldDoneClicked() { - onDoneButtonClicked() - } - - override fun onDoneButtonClicked() { - if (!processValidation()) return - - val twoFaSid = screenState.value.twoFaSid - val twoFaCode = screenState.value.twoFaCode - - coordinator.finishWithResult(TwoFaResult.Success(sid = twoFaSid, code = twoFaCode)) - } - - private fun processValidation(): Boolean { - val isValid = validator.validate(screenState.value).isValid() - - screenState.updateValue( - screenState.value.copy( - codeError = if (isValid) null - else UiText.Simple("Field must not be empty") - ) - ) - - return isValid - } - - private fun sendValidationCode() { - val validationSid = screenState.value.twoFaSid - - viewModelScope.launch { - sendRequest { - authRepository.sendSms(validationSid) - }?.let { response -> - val newValidationType = response.validationType - val newCanResendSms = response.validationResend == "sms" - - screenState.updateValue( - screenState.value.copy( - canResendSms = newCanResendSms, - twoFaText = getTwoFaText( - TwoFaValidationType.parse( - newValidationType ?: "null" - ) - ) - ) - ) - - startTickTimer(response.delay) - } - } - } - - private fun startTickTimer(delay: Int?) { - if (delay == null || delayJob?.isActive.isTrue) return - - delayJob = createTimerFlow( - time = delay, - onStartAction = { - screenState.updateValue( - screenState.value.copy(canResendSms = false) - ) - }, - onTickAction = { remainedTime -> - screenState.updateValue( - screenState.value.copy(delayTime = remainedTime) - ) - }, - onTimeoutAction = { - screenState.updateValue( - screenState.value.copy( - canResendSms = true - ) - ) - }, - ).launchIn(viewModelScope) - } - - private fun getTwoFaText(validationType: TwoFaValidationType): UiText { - return when (validationType) { - TwoFaValidationType.Sms -> UiText.Simple("sms") - TwoFaValidationType.TwoFaApp -> UiText.Simple("2fa app") - is TwoFaValidationType.Another -> UiText.Simple(validationType.type) - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/screen/TwoFaCoordinator.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/screen/TwoFaCoordinator.kt deleted file mode 100644 index 4cbd4e9f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/screen/TwoFaCoordinator.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.meloda.fast.screens.twofa.screen - -import com.github.terrakok.cicerone.Router -import com.meloda.fast.screens.twofa.model.TwoFaResult -import kotlinx.coroutines.flow.MutableSharedFlow - -interface TwoFaCoordinator { - - fun finishWithResult(result: TwoFaResult) -} - -class TwoFaCoordinatorImpl( - private val resultFlow: MutableSharedFlow, - private val router: Router -) : TwoFaCoordinator { - - override fun finishWithResult(result: TwoFaResult) { - resultFlow.tryEmit(result) - router.exit() - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/screen/TwoFaScreen.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/screen/TwoFaScreen.kt deleted file mode 100644 index 8437f6ff..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/screen/TwoFaScreen.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.meloda.fast.screens.twofa.screen - -import com.github.terrakok.cicerone.Router -import com.meloda.fast.base.screen.AppScreen -import com.meloda.fast.base.screen.createResultFlow -import com.meloda.fast.screens.twofa.TwoFaScreens -import com.meloda.fast.screens.twofa.model.TwoFaArguments -import com.meloda.fast.screens.twofa.model.TwoFaResult -import kotlin.properties.Delegates - -class TwoFaScreen : AppScreen { - - override val resultFlow = createResultFlow() - - override var args: TwoFaArguments by Delegates.notNull() - - override fun show(router: Router, args: TwoFaArguments) { - this.args = args - router.navigateTo(TwoFaScreens.twoFaScreen()) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/twofa/validation/TwoFaValidator.kt b/app/src/main/kotlin/com/meloda/fast/screens/twofa/validation/TwoFaValidator.kt deleted file mode 100644 index d3b54e28..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/twofa/validation/TwoFaValidator.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.meloda.fast.screens.twofa.validation - -import com.meloda.fast.screens.twofa.model.TwoFaScreenState -import com.meloda.fast.screens.twofa.model.TwoFaValidationResult - -class TwoFaValidator { - - fun validate(screenState: TwoFaScreenState): TwoFaValidationResult { - return when { - screenState.twoFaCode.isEmpty() -> TwoFaValidationResult.Empty - else -> TwoFaValidationResult.Valid - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/updates/UpdatesFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/updates/UpdatesFragment.kt deleted file mode 100644 index bacb2186..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/updates/UpdatesFragment.kt +++ /dev/null @@ -1,391 +0,0 @@ -package com.meloda.fast.screens.updates - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ArrowBack -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.unit.dp -import androidx.core.os.bundleOf -import com.meloda.fast.R -import com.meloda.fast.base.BaseFragment -import com.meloda.fast.ext.getParcelableCompat -import com.meloda.fast.ext.listenValue -import com.meloda.fast.ext.showDialog -import com.meloda.fast.ext.string -import com.meloda.fast.model.UpdateItem -import com.meloda.fast.model.base.UiText -import com.meloda.fast.screens.updates.model.UpdateState -import com.meloda.fast.ui.AppTheme -import com.meloda.fast.util.AndroidUtils -import okhttp3.ResponseBody -import org.koin.androidx.viewmodel.ext.android.viewModel - - -class UpdatesFragment : BaseFragment(R.layout.fragment_updates) { - - private val viewModel: UpdatesViewModel by viewModel() - - private val changelogPlaceholder by lazy { - string(R.string.fragment_updates_changelog_none) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - UpdatesScreen() - } - } - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun UpdatesScreen() { - AppTheme { - val state by viewModel.screenState.collectAsState() - val updateState = state.updateState - val downloadProgress by viewModel.currentDownloadProgress.collectAsState() - val animatedProgress by animateFloatAsState( - targetValue = downloadProgress / 100f, - animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec - ) - - Scaffold(topBar = { Toolbar() }) { paddingValues -> - Surface( - modifier = Modifier - .padding(paddingValues) - .fillMaxSize() - ) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize(), - ) { - when { - updateState.isLoading() -> CircularProgressIndicator() - updateState.isDownloading() -> { - Text( - text = getString(R.string.fragment_updates_downloading_update), - style = MaterialTheme.typography.bodyLarge - ) - Spacer(modifier = Modifier.height(12.dp)) - if (animatedProgress > 0) { - LinearProgressIndicator(progress = animatedProgress) - } else { - LinearProgressIndicator() - } - Spacer(modifier = Modifier.height(12.dp)) - FilledTonalButton(onClick = viewModel::onCancelDownloadButtonClicked) { - Text(text = getString(R.string.action_stop)) - } - } - - else -> { - getTitle(updateState)?.let { title -> - Text( - text = title, - style = MaterialTheme.typography.headlineSmall - ) - Spacer(modifier = Modifier.height(8.dp)) - } - - getSubtitle(updateState)?.let { subtitle -> - Text( - text = subtitle, - style = MaterialTheme.typography.bodyLarge - ) - Spacer(modifier = Modifier.height(6.dp)) - } - - state.updateItem?.changelog?.let { - Text( - text = getString(R.string.fragment_updates_changelog), - style = TextStyle(textDecoration = TextDecoration.Underline), - modifier = Modifier.clickable(onClick = viewModel::onChangelogButtonClicked) - ) - } - - getActionButtonText(updateState)?.let { buttonText -> - Spacer(modifier = Modifier.height(24.dp)) - ExtendedFloatingActionButton( - onClick = viewModel::onActionButtonClicked, - modifier = Modifier, - text = { Text(text = buttonText) }, - icon = { - getActionButtonIcon(state = updateState)?.let { painter -> - Spacer(modifier = Modifier.width(4.dp)) - Icon(painter = painter, contentDescription = null) - } - } - ) - } - - if (updateState.isDownloaded()) { - Spacer(modifier = Modifier.height(48.dp)) - Text( - text = getString(R.string.fragment_updates_issues_installing), - style = TextStyle(textDecoration = TextDecoration.Underline), - modifier = Modifier.clickable(onClick = viewModel::onIssuesButtonClicked), - ) - } - } - } - } - } - } - } - } - - private fun getTitle(state: UpdateState): String? { - return when (state) { - UpdateState.Error -> R.string.error_occurred - UpdateState.NewUpdate -> R.string.fragment_updates_new_version - UpdateState.NoUpdates -> R.string.fragment_updates_no_updates - UpdateState.Downloaded -> R.string.fragment_updates_downloaded - else -> null - }?.let(requireContext()::getString) - } - - private fun getSubtitle(state: UpdateState): String? { - return when (state) { - UpdateState.Error -> { - viewModel.screenState.value.error?.let { error -> - if (error.contains("cannot be converted", ignoreCase = true) - || error.contains("begin_object", ignoreCase = true) - ) { - "OTA Server is unavailable" - } else { - string(R.string.error_occurred_description, error) - } - } - } - - UpdateState.NewUpdate, UpdateState.Downloaded -> { - viewModel.screenState.value.updateItem?.let { item -> - string( - R.string.fragment_updates_new_version_description, item.versionName - ) - } - } - - UpdateState.NoUpdates -> string(R.string.fragment_updates_no_updates_description) - else -> null - } - } - - private fun getActionButtonText(state: UpdateState): String? { - return when (state) { - UpdateState.Error -> R.string.fragment_updates_try_again - UpdateState.NewUpdate -> R.string.fragment_updates_download_update - UpdateState.NoUpdates -> R.string.fragment_updates_check_updates - UpdateState.Downloaded -> R.string.fragment_updates_install - else -> null - }?.let(requireContext()::getString) - } - - @Composable - private fun getActionButtonIcon(state: UpdateState): Painter? { - return when (state) { - UpdateState.Error -> R.drawable.round_restart_alt_24 - UpdateState.NewUpdate -> R.drawable.round_file_download_24 - UpdateState.Downloaded -> R.drawable.round_install_mobile_24 - else -> null - }?.let { painterResource(id = it) } - } - - @OptIn(ExperimentalMaterial3Api::class) - @Composable - fun Toolbar() { - TopAppBar( - title = { Text(text = "Application updates") }, - navigationIcon = { - IconButton( - onClick = { requireActivity().onBackPressedDispatcher.onBackPressed() } - ) { - Icon( - imageVector = Icons.Rounded.ArrowBack, - contentDescription = null, - ) - } - } - ) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - listenViewModel() - - if (requireArguments().containsKey(ARG_UPDATE_ITEM)) { - val updateItem: UpdateItem = - requireArguments().getParcelableCompat(ARG_UPDATE_ITEM, UpdateItem::class.java) - ?: return - - viewModel.onUpdateItemExists(updateItem) - } else { - viewModel.checkUpdates() - } - } - - private fun listenViewModel() = with(viewModel) { - isNeedToShowChangelogAlert.listenValue(::handleNeedToShowChangelogAlert) - isNeedToShowUnknownSourcesAlert.listenValue(::handleNeedToShowUnknownSourcesAlert) - isNeedToShowIssuesAlert.listenValue(::handleNeedToShowIssuesAlert) - isNeedToShowFileNotFoundAlert.listenValue(::handleNeedToShowFileNotFoundAlert) - } - - private fun handleNeedToShowChangelogAlert(isNeedToShow: Boolean) { - if (isNeedToShow) { - showChangelogAlert() - } - } - - private fun handleNeedToShowUnknownSourcesAlert(isNeedToShow: Boolean) { - if (isNeedToShow) { - showUnknownSourcesAlert() - } - } - - private fun handleNeedToShowIssuesAlert(isNeedToShow: Boolean) { - if (isNeedToShow) { - showIssuesAlert() - } - } - - private fun handleNeedToShowFileNotFoundAlert(isNeedToShow: Boolean) { - if (isNeedToShow) { - showFileNotFoundAlert() - } - } - - private fun showUnknownSourcesAlert() { - context?.showDialog( - title = UiText.Resource(R.string.warning), - message = UiText.Resource(R.string.fragment_updates_unknown_sources_disabled_message), - positiveText = UiText.Resource(R.string.yes), - positiveAction = { AndroidUtils.openInstallUnknownAppsScreen(requireContext()) }, - negativeText = UiText.Resource(R.string.cancel), - onDismissAction = viewModel::onUnknownSourcesAlertDismissed, - isCancelable = false - ) - } - - private fun showChangelogAlert() { - val messageText = - viewModel.screenState.value.updateItem?.changelog?.ifBlank { - changelogPlaceholder - } ?: changelogPlaceholder - - context?.showDialog( - title = UiText.Resource(R.string.fragment_updates_changelog), - message = UiText.Simple(messageText), - positiveText = UiText.Resource(R.string.ok), - onDismissAction = viewModel::onChangelogAlertDismissed - ) - } - - private fun showIssuesAlert() { - context?.showDialog( - message = UiText.Resource(R.string.fragment_updates_issues_description), - positiveText = UiText.Resource(R.string.action_delete), - positiveAction = viewModel::onIssuesAlertPositiveButtonClicked, - negativeText = UiText.Resource(R.string.cancel), - onDismissAction = viewModel::onIssuesAlertDismissed - ) - } - - private fun showFileNotFoundAlert() { - context?.showDialog( - title = UiText.Resource(R.string.warning), - message = UiText.Resource(R.string.fragment_updates_file_not_found_description), - positiveText = UiText.Resource(R.string.ok), - onDismissAction = viewModel::onFileNotFoundAlertDismissed, - isCancelable = false - ) - } - - private fun writeFileToStorage(responseBody: ResponseBody?) { -// if (responseBody == null) return -// -// val updateItem = requireNotNull(viewModel.currentItem.value) -// -// try { -// val destination = requireContext() -// .getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + -// "${File.separator}${updateItem.fileName}.${updateItem.extension}" -// -// val file = File(destination) -// if (file.exists()) file.delete() -// -// var inputStream: InputStream? = null -// var outputStream: OutputStream? = null -// try { -// val fileReader = ByteArray(4096) -// val fileSize: Long = responseBody.contentLength() -// -// requireActivity().runOnUiThread { -// binding.loadingProgress.max = fileSize.toInt() -// binding.loadingProgress.progress = 0 -// } -// -// var fileSizeDownloaded: Long = 0 -// inputStream = responseBody.byteStream() -// outputStream = FileOutputStream(file) -// while (true) { -// val read: Int = inputStream.read(fileReader) -// if (read == -1) { -// break -// } -// outputStream.write(fileReader, 0, read) -// fileSizeDownloaded += read.toLong() -// } -// outputStream.flush() -// -// requireActivity().runOnUiThread { -// installUpdate(file) -// } -// } catch (e: IOException) { -// -// } finally { -// inputStream?.close() -// outputStream?.close() -// } -// } catch (e: IOException) { -// -// } - } - - companion object { - private const val ARG_UPDATE_ITEM = "arg_update_item" - private const val ARG_FILE_BASE_PATH = "file://" - - fun newInstance(updateItem: UpdateItem? = null): UpdatesFragment { - val fragment = UpdatesFragment() - if (updateItem != null) { - fragment.arguments = bundleOf(ARG_UPDATE_ITEM to updateItem) - } else { - fragment.arguments = Bundle() - } - - return fragment - } - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/updates/UpdatesViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/updates/UpdatesViewModel.kt deleted file mode 100644 index f45e3ba6..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/updates/UpdatesViewModel.kt +++ /dev/null @@ -1,375 +0,0 @@ -package com.meloda.fast.screens.updates - -import android.app.DownloadManager -import android.content.Context -import android.content.IntentFilter -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.util.Log -import androidx.core.content.ContextCompat -import androidx.lifecycle.viewModelScope -import com.meloda.fast.R -import com.meloda.fast.base.viewmodel.DeprecatedBaseViewModel -import com.meloda.fast.common.AppConstants -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.common.UpdateManager -import com.meloda.fast.common.UpdateManagerState -import com.meloda.fast.ext.createTimerFlow -import com.meloda.fast.ext.isSdkAtLeast -import com.meloda.fast.ext.listenValue -import com.meloda.fast.model.UpdateItem -import com.meloda.fast.receiver.DownloadManagerReceiver -import com.meloda.fast.screens.updates.model.UpdateState -import com.meloda.fast.screens.updates.model.UpdatesScreenState -import com.meloda.fast.util.AndroidUtils -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.* -import java.io.File -import kotlin.math.roundToInt -import kotlin.time.Duration.Companion.milliseconds - -interface UpdatesViewModel { - val screenState: MutableStateFlow - val currentDownloadProgress: StateFlow - - val isNeedToShowChangelogAlert: Flow - val isNeedToShowUnknownSourcesAlert: Flow - val isNeedToShowIssuesAlert: Flow - val isNeedToShowFileNotFoundAlert: Flow - - fun onUpdateItemExists(updateItem: UpdateItem) - - fun checkUpdates() - - fun onChangelogButtonClicked() - fun onActionButtonClicked() - fun onCancelDownloadButtonClicked() - fun onIssuesButtonClicked() - - fun onChangelogAlertDismissed() - fun onUnknownSourcesAlertDismissed() - fun onIssuesAlertDismissed() - fun onIssuesAlertPositiveButtonClicked() - fun onFileNotFoundAlertDismissed() -} - -class UpdatesViewModelImpl constructor( - private val updateManager: UpdateManager, -) : DeprecatedBaseViewModel(), UpdatesViewModel { - - override val screenState = MutableStateFlow(UpdatesScreenState.EMPTY) - override val currentDownloadProgress = MutableStateFlow(0) - - override val isNeedToShowChangelogAlert = MutableStateFlow(false) - override val isNeedToShowUnknownSourcesAlert = MutableStateFlow(false) - override val isNeedToShowIssuesAlert = MutableStateFlow(false) - override val isNeedToShowFileNotFoundAlert = MutableStateFlow(false) - - private var currentJob: Job? = null - - private val downloadManager by lazy { - AppGlobal.Instance.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager - } - - init { - updateManager.stateFlow.listenValue(::updateState) - } - - override fun onUpdateItemExists(updateItem: UpdateItem) { - val newForm = screenState.value.copy( - updateItem = updateItem, - updateState = UpdateState.NewUpdate, - error = null - ) - screenState.update { newForm } - } - - override fun checkUpdates() { - if (currentJob != null) { - currentJob?.cancel() - currentJob = null - } - - updateUpdateState(UpdateState.Loading) - - currentJob = updateManager.checkUpdates().apply { - invokeOnCompletion { currentJob = null } - } - } - - override fun onChangelogButtonClicked() { - isNeedToShowChangelogAlert.tryEmit(true) - } - - override fun onActionButtonClicked() { - val state = screenState.value.updateState - - if (!state.isDownloaded()) { - downloadUpdate() - return - } - - when (state) { - UpdateState.NewUpdate -> checkIsInstallingAllowed() - UpdateState.NoUpdates, UpdateState.Error -> checkUpdates() - UpdateState.Downloaded -> installUpdate() - else -> Unit - } - } - - override fun onCancelDownloadButtonClicked() { - when (screenState.value.updateState) { - UpdateState.Downloading -> cancelCurrentDownload() - else -> Unit - } - } - - override fun onIssuesButtonClicked() { - isNeedToShowIssuesAlert.tryEmit(true) - } - - override fun onChangelogAlertDismissed() { - isNeedToShowChangelogAlert.tryEmit(false) - } - - override fun onUnknownSourcesAlertDismissed() { - isNeedToShowUnknownSourcesAlert.tryEmit(false) - } - - override fun onIssuesAlertDismissed() { - isNeedToShowIssuesAlert.tryEmit(false) - } - - override fun onIssuesAlertPositiveButtonClicked() { - deleteInstalledFile() - checkUpdates() - } - - override fun onFileNotFoundAlertDismissed() { - isNeedToShowFileNotFoundAlert.tryEmit(false) - checkUpdates() - } - - private fun deleteInstalledFile() { - // TODO: 26.03.2023, Danil Nikolaev: use updateItem - val apkName = "bruhLol" - - val apkFileName = "$apkName.apk" - - val destination = "%s/$apkFileName".format( - AppGlobal.Instance.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() - ) - - val file = File(destination) - if (!file.exists()) return - file.delete() - } - - private fun updateState(updateManagerState: UpdateManagerState) { - val item = UpdateItem.EMPTY -// updateManagerState.updateItem - val error = updateManagerState.throwable - - var fileExists = false - - if (item != null) { - // TODO: 26.03.2023, Danil Nikolaev: use updateItem - val apkName = "bruhLol" - - val apkFileName = "$apkName.apk" - - val destination = "%s/$apkFileName".format( - AppGlobal.Instance.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() - ) - - val file = File(destination) - fileExists = file.exists() - } - - val newUpdateState = when { - item != null -> { - if (fileExists) { - UpdateState.Downloaded - } else { - UpdateState.NewUpdate - } - } - error != null -> UpdateState.Error - else -> UpdateState.NoUpdates - } - updateUpdateState(newUpdateState) - - val newError = error?.message - - val newState = screenState.value.copy( - updateItem = item, - error = newError - ) - screenState.update { newState } - } - - private fun checkIsInstallingAllowed() { - if (!isSdkAtLeast(Build.VERSION_CODES.O) && !AndroidUtils.isCanInstallUnknownApps()) { - isNeedToShowUnknownSourcesAlert.update { true } - } else { - downloadUpdate() - } - } - - private var downloadId: Long? = null - - private fun downloadUpdate() { - val context = AppGlobal.Instance - - updateUpdateState(UpdateState.Loading) -// val newUpdate = screenState.value.updateItem ?: return - - // TODO: 26.03.2023, Danil Nikolaev: use updateItem - val apkName = "bruhLol" - - val apkFileName = "$apkName.apk" - - val destination = "%s/$apkFileName".format( - context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() - ) - - val file = File(destination) - if (file.exists()) { - updateUpdateState(UpdateState.Downloaded) - return - } - - val downloadUri = try { - Uri.parse( - "https://vk.com/doc157582555_635903147?hash=gTEOVno21WCtxX9GclYo8Liloat5V4xt4WB6nSuOMl8&dl=PQvcF2f7jyJDhJzMFOfRzCZXMx0MztmnwzhQYe4Ycdz" - ) - } catch (e: Exception) { - e.printStackTrace() - Uri.EMPTY - } - - val request = DownloadManager.Request(downloadUri).apply { - setTitle("${context.getString(R.string.app_name)} $apkFileName") - setMimeType(AppConstants.INSTALL_APP_MIME_TYPE) - setDestinationInExternalFilesDir( - context, - Environment.DIRECTORY_DOWNLOADS, - apkFileName - ) - setAllowedNetworkTypes( - DownloadManager.Request.NETWORK_WIFI or - DownloadManager.Request.NETWORK_MOBILE - ) - setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - } - - val receiver = DownloadManagerReceiver() - receiver.onReceiveAction = { - downloadId = null - - installUpdate(file) - - context.unregisterReceiver(receiver) - } - - ContextCompat.registerReceiver( - context, - receiver, - IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), - ContextCompat.RECEIVER_NOT_EXPORTED - ) - - downloadId = downloadManager.enqueue(request) - - updateUpdateState(UpdateState.Downloading) - - var isDownloaded = false - - createTimerFlow( - isNeedToEndCondition = { isDownloaded }, - onStartAction = { - Log.d("Downloading update", "downloadUpdate: onStart") - }, - onTickAction = { - val query = DownloadManager.Query() - query.setFilterById(downloadId ?: -1) - - val cursor = downloadManager.query(query) - if (cursor.moveToFirst()) { - val sizeIndex = - cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES) - val downloadedIndex = - cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR) - val size = cursor.getInt(sizeIndex) - val downloaded = cursor.getInt(downloadedIndex) - val progress = if (size != -1) { - downloaded * 100.0F / size - } else { - 0.0F - } - - val intProgress = progress.roundToInt() - if (intProgress >= 1) { - currentDownloadProgress.emit(intProgress) - } - - Log.d("Downloading update", "progress: $progress%") - - if (intProgress >= 100) { - isDownloaded = true - currentDownloadProgress.emit(0) - updateUpdateState(UpdateState.Downloaded) - } - } - }, - onEndAction = {}, - interval = 250.milliseconds - ).launchIn(viewModelScope) - } - - private fun checkDownloadedFileExists(): File? { - // TODO: 26.03.2023, Danil Nikolaev: use updateItem - val apkName = "bruhLol" - - val apkFileName = "$apkName.apk" - - val destination = "%s/$apkFileName".format( - AppGlobal.Instance.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() - ) - - val file = File(destination) - return if (file.exists()) file else null - } - - private fun installUpdate(file: File? = null) { - val context = AppGlobal.Instance - val destinationFile = file ?: checkDownloadedFileExists() ?: run { - isNeedToShowFileNotFoundAlert.tryEmit(true) - return - } - - val installIntent = AndroidUtils.getInstallPackageIntent( - context, - ARG_PROVIDER_PATH, - destinationFile - ) - - context.startActivity(installIntent) - } - - private fun updateUpdateState(newState: UpdateState) { - val newForm = screenState.value.copy(updateState = newState) - screenState.update { newForm } - } - - private fun cancelCurrentDownload() { - currentDownloadProgress.tryEmit(0) - downloadId?.run { downloadManager.remove(this) } - checkUpdates() - } - - companion object { - private const val ARG_PROVIDER_PATH = ".provider" - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/updates/di/UpdatesDI.kt b/app/src/main/kotlin/com/meloda/fast/screens/updates/di/UpdatesDI.kt deleted file mode 100644 index 60634afb..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/updates/di/UpdatesDI.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.meloda.fast.screens.updates.di - -import com.meloda.fast.screens.updates.UpdatesViewModelImpl -import org.koin.androidx.viewmodel.dsl.viewModelOf -import org.koin.dsl.module - -val updatesModule = module { - viewModelOf(::UpdatesViewModelImpl) -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/updates/model/UpdateState.kt b/app/src/main/kotlin/com/meloda/fast/screens/updates/model/UpdateState.kt deleted file mode 100644 index 8ea8b863..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/updates/model/UpdateState.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.meloda.fast.screens.updates.model - -sealed class UpdateState { - object NewUpdate : UpdateState() - object NoUpdates : UpdateState() - object Loading : UpdateState() - object Error : UpdateState() - object Downloading : UpdateState() - object Downloaded : UpdateState() - - fun isNewUpdate() = this == NewUpdate - fun isLoading() = this == Loading - fun isDownloading() = this == Downloading - fun isDownloaded() = this == Downloaded -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/updates/model/UpdatesScreenState.kt b/app/src/main/kotlin/com/meloda/fast/screens/updates/model/UpdatesScreenState.kt deleted file mode 100644 index c99b0dcb..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/updates/model/UpdatesScreenState.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.meloda.fast.screens.updates.model - -import com.meloda.fast.model.UpdateItem - -data class UpdatesScreenState( - val updateItem: UpdateItem?, - val updateState: UpdateState, - val error: String?, - val currentProgress: Float?, - val isProgressIntermediate: Boolean, -) { - - companion object { - val EMPTY = UpdatesScreenState( - updateItem = null, - updateState = UpdateState.NoUpdates, - error = null, - currentProgress = null, - isProgressIntermediate = true - ) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/screens/userbanned/UserBannedFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/userbanned/UserBannedFragment.kt deleted file mode 100644 index 49930a03..00000000 --- a/app/src/main/kotlin/com/meloda/fast/screens/userbanned/UserBannedFragment.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.meloda.fast.screens.userbanned - -import android.os.Bundle -import android.view.View -import androidx.core.os.bundleOf -import by.kirich1409.viewbindingdelegate.viewBinding -import com.meloda.fast.R -import com.meloda.fast.base.BaseFragment -import com.meloda.fast.databinding.FragmentUserBannedBinding -import dev.chrisbanes.insetter.applyInsetter - -class UserBannedFragment : BaseFragment(R.layout.fragment_user_banned) { - - companion object { - - private const val ArgMemberName = "member_name" - private const val ArgMessage = "message" - private const val ArgRestoreUrl = "restore_url" - private const val ArgAccessToken = "access_token" - - fun newInstance( - memberName: String, - message: String, - restoreUrl: String, - accessToken: String - ): UserBannedFragment { - val fragment = UserBannedFragment() - fragment.arguments = bundleOf( - ArgMemberName to memberName, - ArgMessage to message, - ArgRestoreUrl to restoreUrl, - ArgAccessToken to accessToken - ) - return fragment - } - } - - private val binding by viewBinding(FragmentUserBannedBinding::bind) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.root.applyInsetter { - type(navigationBars = true) { padding() } - } - - binding.toolbar.applyInsetter { - type(statusBars = true) { padding() } - } - binding.toolbar.setNavigationOnClickListener { requireActivity().onBackPressedDispatcher.onBackPressed() } - - binding.name.text = requireArguments().getString(ArgMemberName) - binding.reason.text = requireArguments().getString(ArgMessage) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/service/LongPollQSTileService.kt b/app/src/main/kotlin/com/meloda/fast/service/LongPollQSTileService.kt deleted file mode 100644 index bb033821..00000000 --- a/app/src/main/kotlin/com/meloda/fast/service/LongPollQSTileService.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.meloda.fast.service - -import android.content.Intent -import android.service.quicksettings.Tile -import android.service.quicksettings.TileService -import android.util.Log -import com.meloda.fast.screens.main.activity.MainActivity - -class LongPollQSTileService : TileService() { - - override fun onTileAdded() { - Log.d("LongPollQSTileService", "onTileAdded") - super.onTileAdded() - } - - override fun onStartListening() { - qsTile.state = Tile.STATE_ACTIVE - qsTile.updateTile() - Log.d("LongPollQSTileService", "onStartListening") - super.onStartListening() - } - - - override fun onStopListening() { - Log.d("LongPollQSTileService", "onStopListening") - super.onStopListening() - } - - override fun onClick() { - Log.d("LongPollQSTileService", "onClick") - - startActivityAndCollapse(Intent(this, MainActivity::class.java).apply { - putExtra("data", "open_settings") - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - }) - super.onClick() - } - - override fun onTileRemoved() { - Log.d("LongPollQSTileService", "onTileRemoved") - super.onTileRemoved() - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/service/LongPollService.kt b/app/src/main/kotlin/com/meloda/fast/service/LongPollService.kt deleted file mode 100644 index 26fb7c0b..00000000 --- a/app/src/main/kotlin/com/meloda/fast/service/LongPollService.kt +++ /dev/null @@ -1,281 +0,0 @@ -package com.meloda.fast.service - -import android.app.Notification -import android.app.PendingIntent -import android.app.Service -import android.content.Intent -import android.os.IBinder -import android.util.Log -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import androidx.core.content.edit -import com.google.gson.JsonArray -import com.google.gson.JsonObject -import com.meloda.fast.R -import com.meloda.fast.api.VKConstants -import com.meloda.fast.api.base.ApiError -import com.meloda.fast.api.longpoll.LongPollUpdatesParser -import com.meloda.fast.api.model.base.BaseVkLongPoll -import com.meloda.fast.api.network.ApiAnswer -import com.meloda.fast.api.network.longpoll.LongPollGetUpdatesRequest -import com.meloda.fast.api.network.messages.MessagesGetLongPollServerRequest -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.data.messages.MessagesRepository -import com.meloda.fast.ext.isTrue -import com.meloda.fast.receiver.StopLongPollServiceReceiver -import com.meloda.fast.screens.settings.SettingsFragment -import com.meloda.fast.util.NotificationsUtils -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch -import org.koin.android.ext.android.inject -import kotlin.coroutines.CoroutineContext - -class LongPollService : Service() { - - companion object { - const val TAG = "LongPollTask" - - const val KeyLongPollWasDestroyed = "long_poll_was_destroyed" - - private const val NOTIFICATION_ID = 1001 - } - - private val job = SupervisorJob() - - private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> - Log.d(TAG, "error: $throwable") - throwable.printStackTrace() - } - - private val coroutineContext: CoroutineContext - get() = Dispatchers.IO + job + exceptionHandler - - private val coroutineScope = CoroutineScope(coroutineContext) - - private val repository: MessagesRepository by inject() - - private val updatesParser: LongPollUpdatesParser by inject() - - private var asForeground = true - private var foregroundNotification: Notification? = null - - override fun onCreate() { - super.onCreate() - - if (AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, - SettingsFragment.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND - ) - ) { - val notificationBuilder = - NotificationsUtils.createNotification( - context = this, - title = "LongPoll", - contentText = "обновление ваших сообщений в фоне", - notRemovable = false, - channelId = "long_polling", - priority = NotificationsUtils.NotificationPriority.Low, - category = NotificationCompat.CATEGORY_SERVICE, - customNotificationId = NOTIFICATION_ID - ) - - foregroundNotification = notificationBuilder.build() - startForeground(NOTIFICATION_ID, foregroundNotification) - } - } - - override fun onBind(p0: Intent?): IBinder? { - return null - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - asForeground = intent?.getBooleanExtra("foreground", false).isTrue - - Log.d( - "LongPollService", - "onStartCommand: asForeground: $asForeground; flags: $flags; startId: $startId" - ) - - coroutineScope.launch { startPolling().join() } - - val stopIntent = Intent(this, StopLongPollServiceReceiver::class.java).apply { - action = StopLongPollServiceReceiver.ACTION_STOP - putExtra(StopLongPollServiceReceiver.NOTIFICATION_ID, startId) - } - val stopPendingIntent = PendingIntent.getBroadcast( - this, - 1, - stopIntent, - PendingIntent.FLAG_IMMUTABLE - ) - - val action = NotificationCompat.Action( - R.drawable.ic_round_close_24, - getString(R.string.action_stop), - stopPendingIntent - ) - - if (asForeground) { - val notificationBuilder = - NotificationsUtils.createNotification( - context = this, - title = "LongPoll", - contentText = "обновление ваших сообщений в фоне", - notRemovable = false, - channelId = "long_polling", - priority = NotificationsUtils.NotificationPriority.Low, - category = NotificationCompat.CATEGORY_SERVICE, - actions = listOf(action), - customNotificationId = NOTIFICATION_ID - ) - - foregroundNotification = notificationBuilder.build() - - startForeground(NOTIFICATION_ID, foregroundNotification) - } else { - if (foregroundNotification != null) { - NotificationManagerCompat.from(this).cancel(NOTIFICATION_ID) - foregroundNotification = null - } - } - return START_STICKY - } - - private fun startPolling(): Job { - if (job.isCompleted || job.isCancelled) { - Log.d("LongPollService", "job is completed or cancelled. Fuck off") - throw Exception("Job is over") - } - - Log.d("LongPollService", "job started") - - return coroutineScope.launch { - var serverInfo = getServerInfo() - ?: throw ApiError(errorMessage = "bad VK response (server info)") - - var lastUpdatesResponse: JsonObject? = getUpdatesResponse(serverInfo) - ?: throw ApiError(errorMessage = "initiation error: bad VK response (last updates)") - - var failCount = 0 - - while (job.isActive) { - if (lastUpdatesResponse == null) { - failCount++ - serverInfo = getServerInfo() - ?: throw ApiError(errorMessage = "failed retrieving server info after error: bad VK response (server info #2)") - lastUpdatesResponse = getUpdatesResponse(serverInfo) - continue - } - - when (lastUpdatesResponse["failed"]?.asInt) { - 1 -> { - var newTs = lastUpdatesResponse["ts"]?.asInt - if (newTs == null) { - newTs = serverInfo.ts - failCount++ - } - - lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs)) - } - - 2, 3 -> { - serverInfo = getServerInfo() - ?: throw ApiError( - errorMessage = "failed retrieving server info after error: bad VK response (server info #3)" - ) - lastUpdatesResponse = getUpdatesResponse(serverInfo) - } - - else -> { - val newTs = lastUpdatesResponse["ts"]?.asInt - - if (newTs == null) { - failCount++ - } else { - val updates = lastUpdatesResponse["updates"]?.asJsonArray - - if (updates == null) { - failCount++ - } else { - updates.forEach { item -> - item.asJsonArray?.also { - launch { - handleUpdateEvent(it) - } - } ?: failCount++ - } - } - - lastUpdatesResponse = getUpdatesResponse(serverInfo.copy(ts = newTs)) - } - } - } - } - } - } - - private suspend fun getServerInfo(): BaseVkLongPoll? { - val response = repository.getLongPollServer( - MessagesGetLongPollServerRequest( - needPts = true, - version = VKConstants.LP_VERSION - ) - ) - - println("$TAG: serverInfoResponse: $response") - - if (response is ApiAnswer.Error) return null - if (response is ApiAnswer.Success) { - return response.data.response - } - - return null - } - - private suspend fun getUpdatesResponse(server: BaseVkLongPoll): JsonObject? { - val response = repository.getLongPollUpdates( - serverUrl = "https://${server.server}", - params = LongPollGetUpdatesRequest( - key = server.key, - ts = server.ts, - wait = 25, - mode = 2 or 8 or 32 or 64 or 128, - version = VKConstants.LP_VERSION - ) - ) - - println("$TAG: lastUpdateResponse: $response") - - if (response is ApiAnswer.Success) { - return response.data - } - - return null - } - - private fun handleUpdateEvent(eventJson: JsonArray) { - updatesParser.parseNextUpdate(eventJson) - } - - override fun onDestroy() { - Log.d("LongPollService", "onDestroy") - try { - AppGlobal.preferences.edit { - putBoolean(KeyLongPollWasDestroyed, true) - } - job.cancel() - } catch (e: Exception) { - e.printStackTrace() - } - super.onDestroy() - } - - override fun onLowMemory() { - Log.d("LongPollService", "onLowMemory") - super.onLowMemory() - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/service/MyCustomControlService.kt b/app/src/main/kotlin/com/meloda/fast/service/MyCustomControlService.kt deleted file mode 100644 index 9e20b4e0..00000000 --- a/app/src/main/kotlin/com/meloda/fast/service/MyCustomControlService.kt +++ /dev/null @@ -1,59 +0,0 @@ -@file:RequiresApi(Build.VERSION_CODES.R) - -package com.meloda.fast.service - -import android.app.PendingIntent -import android.content.Intent -import android.os.Build -import android.provider.AlarmClock.EXTRA_MESSAGE -import android.service.controls.Control -import android.service.controls.ControlsProviderService -import android.service.controls.DeviceTypes -import android.service.controls.actions.ControlAction -import androidx.annotation.RequiresApi -import com.meloda.fast.screens.main.activity.MainActivity -import kotlinx.coroutines.jdk9.flowPublish -import java.util.concurrent.Flow -import java.util.function.Consumer - -private const val LIGHT_ID = 1234 -private const val LIGHT_TITLE = "Enable Long Polling" -private const val LIGHT_TYPE = DeviceTypes.TYPE_DOOR - - -class MyCustomControlService : ControlsProviderService() { - - override fun createPublisherForAllAvailable() = - flowPublish { - send(createStatelessControl(LIGHT_ID, LIGHT_TITLE, LIGHT_TYPE)) - } - - private fun createStatelessControl(id: Int, title: String, type: Int): Control { - val intent = Intent(this, MainActivity::class.java) - .putExtra(EXTRA_MESSAGE, title) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - val action = PendingIntent.getActivity( - this, - id, - intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - - return Control.StatelessBuilder(id.toString(), action) - .setTitle(title) - .setDeviceType(type) - .build() - } - - override fun createPublisherFor(controlIds: MutableList): Flow.Publisher { - TODO("Not yet implemented") - } - - override fun performControlAction( - controlId: String, - action: ControlAction, - consumer: Consumer - ) { - - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/service/OnlineService.kt b/app/src/main/kotlin/com/meloda/fast/service/OnlineService.kt deleted file mode 100644 index 29a2580e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/service/OnlineService.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.meloda.fast.service - -import android.app.Service -import android.content.Intent -import android.os.IBinder -import android.util.Log -import com.meloda.fast.api.UserConfig -import com.meloda.fast.api.network.account.AccountSetOfflineRequest -import com.meloda.fast.api.network.account.AccountSetOnlineRequest -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.data.account.AccountsRepository -import com.meloda.fast.screens.settings.SettingsFragment -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import org.koin.android.ext.android.inject -import java.util.Timer -import kotlin.concurrent.schedule -import kotlin.coroutines.CoroutineContext - -class OnlineService : Service(), CoroutineScope { - - private val job = SupervisorJob() - - private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> - Log.d(LongPollService.TAG, "error: $throwable") - throwable.printStackTrace() - } - - override val coroutineContext: CoroutineContext - get() = Dispatchers.Default + job + exceptionHandler - - private val repository: AccountsRepository by inject() - - private var timer: Timer? = null - - private var currentJob: Job? = null - - override fun onBind(intent: Intent?): IBinder? = null - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Log.d("OnlineService", "onStartCommand: flags: $flags; startId: $startId") - - if (AppGlobal.preferences.getBoolean( - SettingsFragment.KEY_VISIBILITY_SEND_ONLINE_STATUS, true - ) - ) { - createTimer() - } - - return START_STICKY_COMPATIBILITY - } - - private fun createTimer() { - timer = Timer().apply { - schedule(delay = 0, period = 300 * 1000L) { - setOnline() - } - } - } - - private fun setOnline() { - if (currentJob != null) return - - currentJob = launch { - Log.d("OnlineService", "setOnline()") - - val token = UserConfig.fastToken ?: UserConfig.accessToken - - if (token.isBlank()) { - Log.d("OnlineService", "setOnline: token is empty") - return@launch - } - - val response = repository.setOnline( - AccountSetOnlineRequest( - voip = false, - accessToken = token - ) - ) - Log.d("OnlineService", "setOnline: response: $response") - currentJob = null - } - } - - private suspend fun setOffline() { - Log.d("OnlineService", "setOffline()") - val response = repository.setOffline( - AccountSetOfflineRequest( - accessToken = UserConfig.accessToken - ) - ) - Log.d("OnlineService", "setOffline: response: $response") - } - - override fun onDestroy() { - super.onDestroy() - timer?.cancel() - currentJob?.cancel("OnlineService destroyed") - Log.d("OnlineService", "onDestroy") - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/ui/AppTheme.kt b/app/src/main/kotlin/com/meloda/fast/ui/AppTheme.kt deleted file mode 100644 index a9f2282d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ui/AppTheme.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.meloda.fast.ui - -import android.os.Build -import androidx.compose.material3.ColorScheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import com.meloda.fast.R -import com.meloda.fast.ext.isSystemUsingDarkMode -import com.meloda.fast.ext.isUsingDarkTheme -import com.meloda.fast.ext.isUsingDynamicColors - - -val StandardColorScheme - get() = if (isSystemUsingDarkMode()) DarkColorScheme - else LightColorScheme - -private val LightColorScheme = lightColorScheme() -private val DarkColorScheme = darkColorScheme() - -@Composable -fun dynamicColorScheme(): ColorScheme { - val context = LocalContext.current - return if (isSystemUsingDarkMode()) dynamicDarkColorScheme(context) - else dynamicLightColorScheme(context) -} - -private val googleSansFonts = FontFamily( - Font(R.font.google_sans_regular), - Font(R.font.google_sans_italic, style = FontStyle.Italic), - Font(R.font.google_sans_medium, weight = FontWeight.Medium), - Font( - R.font.google_sans_medium_italic, - weight = FontWeight.Medium, - style = FontStyle.Italic - ), - Font(R.font.google_sans_bold, weight = FontWeight.Bold), - Font( - R.font.google_sans_bold_italic, - weight = FontWeight.Bold, - style = FontStyle.Italic - ), -) - -private val robotoFonts = FontFamily( - Font(R.font.roboto_regular), - // TODO: 27.03.2023, Danil Nikolaev: add all roboto fonts -) - -@Composable -fun AppTheme( - predefinedColorScheme: ColorScheme? = null, - useDarkTheme: Boolean = isUsingDarkTheme(), - useDynamicColors: Boolean = isUsingDynamicColors(), - content: @Composable () -> Unit -) { - val colorScheme: ColorScheme = when { - useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (useDarkTheme) dynamicDarkColorScheme(context) - else dynamicLightColorScheme(context) - } - - useDarkTheme -> DarkColorScheme - else -> LightColorScheme - } - - val typography = MaterialTheme.typography.copy( - displayLarge = MaterialTheme.typography.displayLarge.copy(fontFamily = googleSansFonts), - displayMedium = MaterialTheme.typography.displayMedium.copy(fontFamily = googleSansFonts), - displaySmall = MaterialTheme.typography.displaySmall.copy(fontFamily = googleSansFonts), - headlineLarge = MaterialTheme.typography.headlineLarge.copy(fontFamily = googleSansFonts), - headlineMedium = MaterialTheme.typography.headlineMedium.copy(fontFamily = googleSansFonts), - headlineSmall = MaterialTheme.typography.headlineSmall.copy(fontFamily = googleSansFonts), - bodyLarge = MaterialTheme.typography.bodyLarge.copy(fontFamily = robotoFonts), - bodyMedium = MaterialTheme.typography.bodyMedium.copy(fontFamily = robotoFonts), - bodySmall = MaterialTheme.typography.bodySmall.copy(fontFamily = robotoFonts) - ) - - MaterialTheme( - colorScheme = predefinedColorScheme ?: colorScheme, - typography = typography, - content = content - ) -} diff --git a/app/src/main/kotlin/com/meloda/fast/ui/BlueColorScheme.kt b/app/src/main/kotlin/com/meloda/fast/ui/BlueColorScheme.kt deleted file mode 100644 index 12f27494..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ui/BlueColorScheme.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.meloda.fast.ui - -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme -import com.meloda.fast.ext.isSystemUsingDarkMode -import com.meloda.fast.ui.colors.Blue - -val BlueColorScheme - get() = if (isSystemUsingDarkMode()) BlueDarkColorScheme - else BlueLightColorScheme - -val BlueLightColorScheme = lightColorScheme( - primary = Blue.md_theme_light_primary, - onPrimary = Blue.md_theme_light_onPrimary, - primaryContainer = Blue.md_theme_light_primaryContainer, - onPrimaryContainer = Blue.md_theme_light_onPrimaryContainer, - secondary = Blue.md_theme_light_secondary, - onSecondary = Blue.md_theme_light_onSecondary, - secondaryContainer = Blue.md_theme_light_secondaryContainer, - onSecondaryContainer = Blue.md_theme_light_onSecondaryContainer, - tertiary = Blue.md_theme_light_tertiary, - onTertiary = Blue.md_theme_light_onTertiary, - tertiaryContainer = Blue.md_theme_light_tertiaryContainer, - onTertiaryContainer = Blue.md_theme_light_onTertiaryContainer, - error = Blue.md_theme_light_error, - errorContainer = Blue.md_theme_light_errorContainer, - onError = Blue.md_theme_light_onError, - onErrorContainer = Blue.md_theme_light_onErrorContainer, - background = Blue.md_theme_light_background, - onBackground = Blue.md_theme_light_onBackground, - surface = Blue.md_theme_light_surface, - onSurface = Blue.md_theme_light_onSurface, - surfaceVariant = Blue.md_theme_light_surfaceVariant, - onSurfaceVariant = Blue.md_theme_light_onSurfaceVariant, - outline = Blue.md_theme_light_outline, - inverseOnSurface = Blue.md_theme_light_inverseOnSurface, - inverseSurface = Blue.md_theme_light_inverseSurface, - inversePrimary = Blue.md_theme_light_inversePrimary, - surfaceTint = Blue.md_theme_light_surfaceTint, - outlineVariant = Blue.md_theme_light_outlineVariant, - scrim = Blue.md_theme_light_scrim, -) - -val BlueDarkColorScheme = darkColorScheme( - primary = Blue.md_theme_dark_primary, - onPrimary = Blue.md_theme_dark_onPrimary, - primaryContainer = Blue.md_theme_dark_primaryContainer, - onPrimaryContainer = Blue.md_theme_dark_onPrimaryContainer, - secondary = Blue.md_theme_dark_secondary, - onSecondary = Blue.md_theme_dark_onSecondary, - secondaryContainer = Blue.md_theme_dark_secondaryContainer, - onSecondaryContainer = Blue.md_theme_dark_onSecondaryContainer, - tertiary = Blue.md_theme_dark_tertiary, - onTertiary = Blue.md_theme_dark_onTertiary, - tertiaryContainer = Blue.md_theme_dark_tertiaryContainer, - onTertiaryContainer = Blue.md_theme_dark_onTertiaryContainer, - error = Blue.md_theme_dark_error, - errorContainer = Blue.md_theme_dark_errorContainer, - onError = Blue.md_theme_dark_onError, - onErrorContainer = Blue.md_theme_dark_onErrorContainer, - background = Blue.md_theme_dark_background, - onBackground = Blue.md_theme_dark_onBackground, - surface = Blue.md_theme_dark_surface, - onSurface = Blue.md_theme_dark_onSurface, - surfaceVariant = Blue.md_theme_dark_surfaceVariant, - onSurfaceVariant = Blue.md_theme_dark_onSurfaceVariant, - outline = Blue.md_theme_dark_outline, - inverseOnSurface = Blue.md_theme_dark_inverseOnSurface, - inverseSurface = Blue.md_theme_dark_inverseSurface, - inversePrimary = Blue.md_theme_dark_inversePrimary, - surfaceTint = Blue.md_theme_dark_surfaceTint, - outlineVariant = Blue.md_theme_dark_outlineVariant, - scrim = Blue.md_theme_dark_scrim, -) diff --git a/app/src/main/kotlin/com/meloda/fast/ui/GreenColorScheme.kt b/app/src/main/kotlin/com/meloda/fast/ui/GreenColorScheme.kt deleted file mode 100644 index 79e9dc77..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ui/GreenColorScheme.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.meloda.fast.ui - -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme -import com.meloda.fast.ext.isSystemUsingDarkMode -import com.meloda.fast.ui.colors.Green - -val GreenColorScheme - get() = if (isSystemUsingDarkMode()) GreenDarkColorScheme - else GreenLightColorScheme - -val GreenLightColorScheme = lightColorScheme( - primary = Green.md_theme_light_primary, - onPrimary = Green.md_theme_light_onPrimary, - primaryContainer = Green.md_theme_light_primaryContainer, - onPrimaryContainer = Green.md_theme_light_onPrimaryContainer, - secondary = Green.md_theme_light_secondary, - onSecondary = Green.md_theme_light_onSecondary, - secondaryContainer = Green.md_theme_light_secondaryContainer, - onSecondaryContainer = Green.md_theme_light_onSecondaryContainer, - tertiary = Green.md_theme_light_tertiary, - onTertiary = Green.md_theme_light_onTertiary, - tertiaryContainer = Green.md_theme_light_tertiaryContainer, - onTertiaryContainer = Green.md_theme_light_onTertiaryContainer, - error = Green.md_theme_light_error, - errorContainer = Green.md_theme_light_errorContainer, - onError = Green.md_theme_light_onError, - onErrorContainer = Green.md_theme_light_onErrorContainer, - background = Green.md_theme_light_background, - onBackground = Green.md_theme_light_onBackground, - surface = Green.md_theme_light_surface, - onSurface = Green.md_theme_light_onSurface, - surfaceVariant = Green.md_theme_light_surfaceVariant, - onSurfaceVariant = Green.md_theme_light_onSurfaceVariant, - outline = Green.md_theme_light_outline, - inverseOnSurface = Green.md_theme_light_inverseOnSurface, - inverseSurface = Green.md_theme_light_inverseSurface, - inversePrimary = Green.md_theme_light_inversePrimary, - surfaceTint = Green.md_theme_light_surfaceTint, - outlineVariant = Green.md_theme_light_outlineVariant, - scrim = Green.md_theme_light_scrim, -) - -val GreenDarkColorScheme = darkColorScheme( - primary = Green.md_theme_dark_primary, - onPrimary = Green.md_theme_dark_onPrimary, - primaryContainer = Green.md_theme_dark_primaryContainer, - onPrimaryContainer = Green.md_theme_dark_onPrimaryContainer, - secondary = Green.md_theme_dark_secondary, - onSecondary = Green.md_theme_dark_onSecondary, - secondaryContainer = Green.md_theme_dark_secondaryContainer, - onSecondaryContainer = Green.md_theme_dark_onSecondaryContainer, - tertiary = Green.md_theme_dark_tertiary, - onTertiary = Green.md_theme_dark_onTertiary, - tertiaryContainer = Green.md_theme_dark_tertiaryContainer, - onTertiaryContainer = Green.md_theme_dark_onTertiaryContainer, - error = Green.md_theme_dark_error, - errorContainer = Green.md_theme_dark_errorContainer, - onError = Green.md_theme_dark_onError, - onErrorContainer = Green.md_theme_dark_onErrorContainer, - background = Green.md_theme_dark_background, - onBackground = Green.md_theme_dark_onBackground, - surface = Green.md_theme_dark_surface, - onSurface = Green.md_theme_dark_onSurface, - surfaceVariant = Green.md_theme_dark_surfaceVariant, - onSurfaceVariant = Green.md_theme_dark_onSurfaceVariant, - outline = Green.md_theme_dark_outline, - inverseOnSurface = Green.md_theme_dark_inverseOnSurface, - inverseSurface = Green.md_theme_dark_inverseSurface, - inversePrimary = Green.md_theme_dark_inversePrimary, - surfaceTint = Green.md_theme_dark_surfaceTint, - outlineVariant = Green.md_theme_dark_outlineVariant, - scrim = Green.md_theme_dark_scrim, -) diff --git a/app/src/main/kotlin/com/meloda/fast/ui/RedColorScheme.kt b/app/src/main/kotlin/com/meloda/fast/ui/RedColorScheme.kt deleted file mode 100644 index f270a59f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ui/RedColorScheme.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.meloda.fast.ui - -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme -import com.meloda.fast.ext.isSystemUsingDarkMode -import com.meloda.fast.ui.colors.Red - -val RedColorScheme - get() = if (isSystemUsingDarkMode()) RedDarkColorScheme - else RedLightColorScheme - -val RedLightColorScheme = lightColorScheme( - primary = Red.md_theme_light_primary, - onPrimary = Red.md_theme_light_onPrimary, - primaryContainer = Red.md_theme_light_primaryContainer, - onPrimaryContainer = Red.md_theme_light_onPrimaryContainer, - secondary = Red.md_theme_light_secondary, - onSecondary = Red.md_theme_light_onSecondary, - secondaryContainer = Red.md_theme_light_secondaryContainer, - onSecondaryContainer = Red.md_theme_light_onSecondaryContainer, - tertiary = Red.md_theme_light_tertiary, - onTertiary = Red.md_theme_light_onTertiary, - tertiaryContainer = Red.md_theme_light_tertiaryContainer, - onTertiaryContainer = Red.md_theme_light_onTertiaryContainer, - error = Red.md_theme_light_error, - errorContainer = Red.md_theme_light_errorContainer, - onError = Red.md_theme_light_onError, - onErrorContainer = Red.md_theme_light_onErrorContainer, - background = Red.md_theme_light_background, - onBackground = Red.md_theme_light_onBackground, - surface = Red.md_theme_light_surface, - onSurface = Red.md_theme_light_onSurface, - surfaceVariant = Red.md_theme_light_surfaceVariant, - onSurfaceVariant = Red.md_theme_light_onSurfaceVariant, - outline = Red.md_theme_light_outline, - inverseOnSurface = Red.md_theme_light_inverseOnSurface, - inverseSurface = Red.md_theme_light_inverseSurface, - inversePrimary = Red.md_theme_light_inversePrimary, - surfaceTint = Red.md_theme_light_surfaceTint, - outlineVariant = Red.md_theme_light_outlineVariant, - scrim = Red.md_theme_light_scrim, -) - -val RedDarkColorScheme = darkColorScheme( - primary = Red.md_theme_dark_primary, - onPrimary = Red.md_theme_dark_onPrimary, - primaryContainer = Red.md_theme_dark_primaryContainer, - onPrimaryContainer = Red.md_theme_dark_onPrimaryContainer, - secondary = Red.md_theme_dark_secondary, - onSecondary = Red.md_theme_dark_onSecondary, - secondaryContainer = Red.md_theme_dark_secondaryContainer, - onSecondaryContainer = Red.md_theme_dark_onSecondaryContainer, - tertiary = Red.md_theme_dark_tertiary, - onTertiary = Red.md_theme_dark_onTertiary, - tertiaryContainer = Red.md_theme_dark_tertiaryContainer, - onTertiaryContainer = Red.md_theme_dark_onTertiaryContainer, - error = Red.md_theme_dark_error, - errorContainer = Red.md_theme_dark_errorContainer, - onError = Red.md_theme_dark_onError, - onErrorContainer = Red.md_theme_dark_onErrorContainer, - background = Red.md_theme_dark_background, - onBackground = Red.md_theme_dark_onBackground, - surface = Red.md_theme_dark_surface, - onSurface = Red.md_theme_dark_onSurface, - surfaceVariant = Red.md_theme_dark_surfaceVariant, - onSurfaceVariant = Red.md_theme_dark_onSurfaceVariant, - outline = Red.md_theme_dark_outline, - inverseOnSurface = Red.md_theme_dark_inverseOnSurface, - inverseSurface = Red.md_theme_dark_inverseSurface, - inversePrimary = Red.md_theme_dark_inversePrimary, - surfaceTint = Red.md_theme_dark_surfaceTint, - outlineVariant = Red.md_theme_dark_outlineVariant, - scrim = Red.md_theme_dark_scrim, -) diff --git a/app/src/main/kotlin/com/meloda/fast/ui/colors/Blue.kt b/app/src/main/kotlin/com/meloda/fast/ui/colors/Blue.kt deleted file mode 100644 index 6d37c3e8..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ui/colors/Blue.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.meloda.fast.ui.colors - -import androidx.compose.ui.graphics.Color - -object Blue { - val md_theme_light_primary = Color(0xFF1059C6) - val md_theme_light_onPrimary = Color(0xFFFFFFFF) - val md_theme_light_primaryContainer = Color(0xFFD9E2FF) - val md_theme_light_onPrimaryContainer = Color(0xFF001945) - val md_theme_light_secondary = Color(0xFF6F4DA0) - val md_theme_light_onSecondary = Color(0xFFFFFFFF) - val md_theme_light_secondaryContainer = Color(0xFFEDDCFF) - val md_theme_light_onSecondaryContainer = Color(0xFF280056) - val md_theme_light_tertiary = Color(0xFF725572) - val md_theme_light_onTertiary = Color(0xFFFFFFFF) - val md_theme_light_tertiaryContainer = Color(0xFFFDD7FA) - val md_theme_light_onTertiaryContainer = Color(0xFF2A132C) - val md_theme_light_error = Color(0xFFBA1A1A) - val md_theme_light_errorContainer = Color(0xFFFFDAD6) - val md_theme_light_onError = Color(0xFFFFFFFF) - val md_theme_light_onErrorContainer = Color(0xFF410002) - val md_theme_light_background = Color(0xFFFEFBFF) - val md_theme_light_onBackground = Color(0xFF1B1B1F) - val md_theme_light_surface = Color(0xFFFEFBFF) - val md_theme_light_onSurface = Color(0xFF1B1B1F) - val md_theme_light_surfaceVariant = Color(0xFFE1E2EC) - val md_theme_light_onSurfaceVariant = Color(0xFF44464F) - val md_theme_light_outline = Color(0xFF757780) - val md_theme_light_inverseOnSurface = Color(0xFFF2F0F4) - val md_theme_light_inverseSurface = Color(0xFF303034) - val md_theme_light_inversePrimary = Color(0xFFB0C6FF) - val md_theme_light_shadow = Color(0xFF000000) - val md_theme_light_surfaceTint = Color(0xFF1059C6) - val md_theme_light_outlineVariant = Color(0xFFC5C6D0) - val md_theme_light_scrim = Color(0xFF000000) - - val md_theme_dark_primary = Color(0xFFB0C6FF) - val md_theme_dark_onPrimary = Color(0xFF002D6E) - val md_theme_dark_primaryContainer = Color(0xFF00429B) - val md_theme_dark_onPrimaryContainer = Color(0xFFD9E2FF) - val md_theme_dark_secondary = Color(0xFFD7BAFF) - val md_theme_dark_onSecondary = Color(0xFF3F1C6E) - val md_theme_dark_secondaryContainer = Color(0xFF563587) - val md_theme_dark_onSecondaryContainer = Color(0xFFEDDCFF) - val md_theme_dark_tertiary = Color(0xFFE0BBDE) - val md_theme_dark_onTertiary = Color(0xFF412742) - val md_theme_dark_tertiaryContainer = Color(0xFF593D5A) - val md_theme_dark_onTertiaryContainer = Color(0xFFFDD7FA) - val md_theme_dark_error = Color(0xFFFFB4AB) - val md_theme_dark_errorContainer = Color(0xFF93000A) - val md_theme_dark_onError = Color(0xFF690005) - val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) - val md_theme_dark_background = Color(0xFF1B1B1F) - val md_theme_dark_onBackground = Color(0xFFE3E2E6) - val md_theme_dark_surface = Color(0xFF1B1B1F) - val md_theme_dark_onSurface = Color(0xFFE3E2E6) - val md_theme_dark_surfaceVariant = Color(0xFF44464F) - val md_theme_dark_onSurfaceVariant = Color(0xFFC5C6D0) - val md_theme_dark_outline = Color(0xFF8F9099) - val md_theme_dark_inverseOnSurface = Color(0xFF1B1B1F) - val md_theme_dark_inverseSurface = Color(0xFFE3E2E6) - val md_theme_dark_inversePrimary = Color(0xFF1059C6) - val md_theme_dark_shadow = Color(0xFF000000) - val md_theme_dark_surfaceTint = Color(0xFFB0C6FF) - val md_theme_dark_outlineVariant = Color(0xFF44464F) - val md_theme_dark_scrim = Color(0xFF000000) - - - val seed = Color(0xFF3771DF) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/ui/colors/Green.kt b/app/src/main/kotlin/com/meloda/fast/ui/colors/Green.kt deleted file mode 100644 index f21e7c4d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ui/colors/Green.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.meloda.fast.ui.colors - -import androidx.compose.ui.graphics.Color - -object Green { - val md_theme_light_primary = Color(0xFF006E29) - val md_theme_light_onPrimary = Color(0xFFFFFFFF) - val md_theme_light_primaryContainer = Color(0xFF94F99D) - val md_theme_light_onPrimaryContainer = Color(0xFF002107) - val md_theme_light_secondary = Color(0xFF516350) - val md_theme_light_onSecondary = Color(0xFFFFFFFF) - val md_theme_light_secondaryContainer = Color(0xFFD4E8D0) - val md_theme_light_onSecondaryContainer = Color(0xFF0F1F10) - val md_theme_light_tertiary = Color(0xFF39656C) - val md_theme_light_onTertiary = Color(0xFFFFFFFF) - val md_theme_light_tertiaryContainer = Color(0xFFBCEAF2) - val md_theme_light_onTertiaryContainer = Color(0xFF001F24) - val md_theme_light_error = Color(0xFFBA1A1A) - val md_theme_light_errorContainer = Color(0xFFFFDAD6) - val md_theme_light_onError = Color(0xFFFFFFFF) - val md_theme_light_onErrorContainer = Color(0xFF410002) - val md_theme_light_background = Color(0xFFFCFDF7) - val md_theme_light_onBackground = Color(0xFF1A1C19) - val md_theme_light_surface = Color(0xFFFCFDF7) - val md_theme_light_onSurface = Color(0xFF1A1C19) - val md_theme_light_surfaceVariant = Color(0xFFDEE5D9) - val md_theme_light_onSurfaceVariant = Color(0xFF424940) - val md_theme_light_outline = Color(0xFF727970) - val md_theme_light_inverseOnSurface = Color(0xFFF0F1EB) - val md_theme_light_inverseSurface = Color(0xFF2F312D) - val md_theme_light_inversePrimary = Color(0xFF79DC84) - val md_theme_light_shadow = Color(0xFF000000) - val md_theme_light_surfaceTint = Color(0xFF006E29) - val md_theme_light_outlineVariant = Color(0xFFC2C9BE) - val md_theme_light_scrim = Color(0xFF000000) - - val md_theme_dark_primary = Color(0xFF79DC84) - val md_theme_dark_onPrimary = Color(0xFF003911) - val md_theme_dark_primaryContainer = Color(0xFF00531D) - val md_theme_dark_onPrimaryContainer = Color(0xFF94F99D) - val md_theme_dark_secondary = Color(0xFFB8CCB5) - val md_theme_dark_onSecondary = Color(0xFF243424) - val md_theme_dark_secondaryContainer = Color(0xFF3A4B3A) - val md_theme_dark_onSecondaryContainer = Color(0xFFD4E8D0) - val md_theme_dark_tertiary = Color(0xFFA1CED6) - val md_theme_dark_onTertiary = Color(0xFF00363D) - val md_theme_dark_tertiaryContainer = Color(0xFF1F4D54) - val md_theme_dark_onTertiaryContainer = Color(0xFFBCEAF2) - val md_theme_dark_error = Color(0xFFFFB4AB) - val md_theme_dark_errorContainer = Color(0xFF93000A) - val md_theme_dark_onError = Color(0xFF690005) - val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) - val md_theme_dark_background = Color(0xFF1A1C19) - val md_theme_dark_onBackground = Color(0xFFE2E3DD) - val md_theme_dark_surface = Color(0xFF1A1C19) - val md_theme_dark_onSurface = Color(0xFFE2E3DD) - val md_theme_dark_surfaceVariant = Color(0xFF424940) - val md_theme_dark_onSurfaceVariant = Color(0xFFC2C9BE) - val md_theme_dark_outline = Color(0xFF8C9389) - val md_theme_dark_inverseOnSurface = Color(0xFF1A1C19) - val md_theme_dark_inverseSurface = Color(0xFFE2E3DD) - val md_theme_dark_inversePrimary = Color(0xFF006E29) - val md_theme_dark_shadow = Color(0xFF000000) - val md_theme_dark_surfaceTint = Color(0xFF79DC84) - val md_theme_dark_outlineVariant = Color(0xFF424940) - val md_theme_dark_scrim = Color(0xFF000000) - - - val seed = Color(0xFF22893C) - -} diff --git a/app/src/main/kotlin/com/meloda/fast/ui/colors/Red.kt b/app/src/main/kotlin/com/meloda/fast/ui/colors/Red.kt deleted file mode 100644 index 4f0aa966..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ui/colors/Red.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.meloda.fast.ui.colors - -import androidx.compose.ui.graphics.Color - -object Red { - val md_theme_light_primary = Color(0xFFA43A3A) - val md_theme_light_onPrimary = Color(0xFFFFFFFF) - val md_theme_light_primaryContainer = Color(0xFFFFDAD8) - val md_theme_light_onPrimaryContainer = Color(0xFF410006) - val md_theme_light_secondary = Color(0xFF775654) - val md_theme_light_onSecondary = Color(0xFFFFFFFF) - val md_theme_light_secondaryContainer = Color(0xFFFFDAD8) - val md_theme_light_onSecondaryContainer = Color(0xFF2C1514) - val md_theme_light_tertiary = Color(0xFF735A2E) - val md_theme_light_onTertiary = Color(0xFFFFFFFF) - val md_theme_light_tertiaryContainer = Color(0xFFFFDEA9) - val md_theme_light_onTertiaryContainer = Color(0xFF271900) - val md_theme_light_error = Color(0xFFBA1A1A) - val md_theme_light_errorContainer = Color(0xFFFFDAD6) - val md_theme_light_onError = Color(0xFFFFFFFF) - val md_theme_light_onErrorContainer = Color(0xFF410002) - val md_theme_light_background = Color(0xFFFFFBFF) - val md_theme_light_onBackground = Color(0xFF201A1A) - val md_theme_light_surface = Color(0xFFFFFBFF) - val md_theme_light_onSurface = Color(0xFF201A1A) - val md_theme_light_surfaceVariant = Color(0xFFF4DDDC) - val md_theme_light_onSurfaceVariant = Color(0xFF534342) - val md_theme_light_outline = Color(0xFF857372) - val md_theme_light_inverseOnSurface = Color(0xFFFBEEEC) - val md_theme_light_inverseSurface = Color(0xFF362F2E) - val md_theme_light_inversePrimary = Color(0xFFFFB3AF) - val md_theme_light_shadow = Color(0xFF000000) - val md_theme_light_surfaceTint = Color(0xFFA43A3A) - val md_theme_light_outlineVariant = Color(0xFFD7C1C0) - val md_theme_light_scrim = Color(0xFF000000) - - val md_theme_dark_primary = Color(0xFFFFB3AF) - val md_theme_dark_onPrimary = Color(0xFF650912) - val md_theme_dark_primaryContainer = Color(0xFF842225) - val md_theme_dark_onPrimaryContainer = Color(0xFFFFDAD8) - val md_theme_dark_secondary = Color(0xFFE7BDBA) - val md_theme_dark_onSecondary = Color(0xFF442928) - val md_theme_dark_secondaryContainer = Color(0xFF5D3F3E) - val md_theme_dark_onSecondaryContainer = Color(0xFFFFDAD8) - val md_theme_dark_tertiary = Color(0xFFE3C28C) - val md_theme_dark_onTertiary = Color(0xFF412D05) - val md_theme_dark_tertiaryContainer = Color(0xFF594319) - val md_theme_dark_onTertiaryContainer = Color(0xFFFFDEA9) - val md_theme_dark_error = Color(0xFFFFB4AB) - val md_theme_dark_errorContainer = Color(0xFF93000A) - val md_theme_dark_onError = Color(0xFF690005) - val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) - val md_theme_dark_background = Color(0xFF201A1A) - val md_theme_dark_onBackground = Color(0xFFEDE0DE) - val md_theme_dark_surface = Color(0xFF201A1A) - val md_theme_dark_onSurface = Color(0xFFEDE0DE) - val md_theme_dark_surfaceVariant = Color(0xFF534342) - val md_theme_dark_onSurfaceVariant = Color(0xFFD7C1C0) - val md_theme_dark_outline = Color(0xFFA08C8B) - val md_theme_dark_inverseOnSurface = Color(0xFF201A1A) - val md_theme_dark_inverseSurface = Color(0xFFEDE0DE) - val md_theme_dark_inversePrimary = Color(0xFFA43A3A) - val md_theme_dark_shadow = Color(0xFF000000) - val md_theme_dark_surfaceTint = Color(0xFFFFB3AF) - val md_theme_dark_outlineVariant = Color(0xFF534342) - val md_theme_dark_scrim = Color(0xFF000000) - - - val seed = Color(0xFFC55251) -} diff --git a/app/src/main/kotlin/com/meloda/fast/ui/widgets/AsyncImage.kt b/app/src/main/kotlin/com/meloda/fast/ui/widgets/AsyncImage.kt deleted file mode 100644 index 68c5fc89..00000000 --- a/app/src/main/kotlin/com/meloda/fast/ui/widgets/AsyncImage.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.meloda.fast.ui.widgets - -import androidx.compose.foundation.Image -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalView -import coil.compose.AsyncImage -import coil.request.ImageRequest - - -/** - * Simple wrapper for coil's AsyncImage for showing preview - * @param contentDescription text used by accessibility services to describe what this image - * represents. This should always be provided unless this image is used for decorative purposes, - * and does not represent a meaningful action that a user can take. This text should be - * localized, such as by using [androidx.compose.ui.res.stringResource] or similar - * @param modifier Modifier used to adjust the layout algorithm or draw decoration content (ex. - * background) - * @param model Either an [ImageRequest] or the [ImageRequest.data] value. - * @param contentScale Optional scale parameter used to determine the aspect ratio scaling to be used - * @param previewPainter Optional painter for preview - */ - -@Composable -fun CoilImage( - contentDescription: String?, - modifier: Modifier, - model: Any?, - contentScale: ContentScale = ContentScale.Fit, - previewPainter: Painter? -) { - if (previewPainter != null && LocalView.current.isInEditMode) { - Image( - painter = previewPainter, - contentDescription = contentDescription, - modifier = modifier - ) - } else { - AsyncImage( - model = model, - contentDescription = contentDescription, - contentScale = contentScale, - modifier = modifier - ) - } -} diff --git a/app/src/main/kotlin/com/meloda/fast/util/ColorUtils.kt b/app/src/main/kotlin/com/meloda/fast/util/ColorUtils.kt deleted file mode 100644 index db82c31c..00000000 --- a/app/src/main/kotlin/com/meloda/fast/util/ColorUtils.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.meloda.fast.util - -import android.graphics.Color - -object ColorUtils { - - - fun alphaColor(color: Int, alphaFactor: Float): Int { - val alpha = Color.alpha(color) - - val red = Color.red(color) - val green = Color.green(color) - val blue = Color.blue(color) - - return Color.argb((alpha * alphaFactor).toInt(), red, green, blue) - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/util/TimeUtils.kt b/app/src/main/kotlin/com/meloda/fast/util/TimeUtils.kt deleted file mode 100644 index 494655bd..00000000 --- a/app/src/main/kotlin/com/meloda/fast/util/TimeUtils.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.meloda.fast.util - -import android.content.Context -import com.meloda.fast.R -import java.text.SimpleDateFormat -import java.util.* -import java.util.concurrent.TimeUnit - -object TimeUtils { - - val OneDayInSeconds get() = TimeUnit.DAYS.toSeconds(1) - - fun removeTime(date: Date): Long { - return Calendar.getInstance().apply { - time = date - this[Calendar.HOUR_OF_DAY] = 0 - this[Calendar.MINUTE] = 0 - this[Calendar.SECOND] = 0 - this[Calendar.MILLISECOND] = 0 - }.timeInMillis - } - - fun getLocalizedDate(context: Context, date: Long): String { - val now = Calendar.getInstance() - val then = Calendar.getInstance().also { it.timeInMillis = date } - - val pattern = when { - now[Calendar.YEAR] != then[Calendar.YEAR] -> "dd MMM yyyy" - now[Calendar.MONTH] != then[Calendar.MONTH] -> "dd MMMM" - now[Calendar.DAY_OF_MONTH] != then[Calendar.DAY_OF_MONTH] -> { - if (now[Calendar.DAY_OF_MONTH] - then[Calendar.DAY_OF_MONTH] == 1) { - return context.getString(R.string.yesterday) - } else { - "dd MMMM" - } - } - else -> return context.getString(R.string.today) - } - - return SimpleDateFormat(pattern, Locale.getDefault()).format(date) - } - - fun getLocalizedTime(context: Context, date: Long): String { - val now = Calendar.getInstance() - val then = Calendar.getInstance().also { it.timeInMillis = date } - - return when { - now[Calendar.YEAR] != then[Calendar.YEAR] -> { - "${now[Calendar.YEAR] - then[Calendar.YEAR]}${ - context.getString(R.string.year_short).lowercase() - }" - } - now[Calendar.MONTH] != then[Calendar.MONTH] -> { - "${now[Calendar.MONTH] - then[Calendar.MONTH]}${ - context.getString(R.string.month_short).lowercase() - }" - } - now[Calendar.DAY_OF_MONTH] != then[Calendar.DAY_OF_MONTH] -> { - val change = now[Calendar.DAY_OF_MONTH] - then[Calendar.DAY_OF_MONTH] - if (change >= 7) { - "${change / 7}${context.getString(R.string.week_short).lowercase()}" - } else { - "$change${context.getString(R.string.day_short).lowercase()}" - } - } - else -> { - if (now[Calendar.MINUTE] == then[Calendar.MINUTE]) { - context.getString(R.string.time_now).lowercase() - } else { - SimpleDateFormat("HH:mm", Locale.getDefault()).format(date) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/view/BoundedLinearLayout.kt b/app/src/main/kotlin/com/meloda/fast/view/BoundedLinearLayout.kt deleted file mode 100644 index 9db7d758..00000000 --- a/app/src/main/kotlin/com/meloda/fast/view/BoundedLinearLayout.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.meloda.fast.view - -import android.annotation.SuppressLint -import android.content.Context -import android.util.AttributeSet -import androidx.appcompat.widget.LinearLayoutCompat -import androidx.core.content.withStyledAttributes -import com.meloda.fast.R - -@SuppressLint("CustomViewStyleable") -class BoundedLinearLayout : LinearLayoutCompat { - private var mBoundedWidth: Int = 0 - private var mBoundedHeight: Int = 0 - - constructor(context: Context) : super(context) { - mBoundedWidth = 0 - mBoundedHeight = 0 - } - - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { - context.withStyledAttributes(attrs, R.styleable.BoundedView) { - mBoundedWidth = getDimensionPixelSize(R.styleable.BoundedView_bounded_width, 0) - mBoundedHeight = getDimensionPixelSize(R.styleable.BoundedView_bounded_height, 0) - } - } - - var maxWidth: Int - get() = mBoundedWidth - set(width) { - if (mBoundedWidth != width) { - mBoundedWidth = width - requestLayout() - } - } - - var maxHeight: Int - get() = mBoundedHeight - set(height) { - if (mBoundedHeight != height) { - mBoundedHeight = height - requestLayout() - } - } - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - var newWidthMeasureSpec = widthMeasureSpec - var newHeightMeasureSpec = heightMeasureSpec - - val measuredWidth = MeasureSpec.getSize(newWidthMeasureSpec) - if (mBoundedWidth in 1 until measuredWidth) { - val measureMode = MeasureSpec.getMode(newWidthMeasureSpec) - newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mBoundedWidth, measureMode) - } - - val measuredHeight = MeasureSpec.getSize(newHeightMeasureSpec) - if (mBoundedHeight in 1 until measuredHeight) { - val measureMode = MeasureSpec.getMode(newHeightMeasureSpec) - newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(mBoundedHeight, measureMode) - } - super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/view/CircleImageView.kt b/app/src/main/kotlin/com/meloda/fast/view/CircleImageView.kt deleted file mode 100644 index 6e574c3f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/view/CircleImageView.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.meloda.fast.view - -import android.content.Context -import android.graphics.Canvas -import android.graphics.Path -import android.graphics.RectF -import android.util.AttributeSet -import androidx.appcompat.widget.AppCompatImageView -import androidx.core.view.doOnPreDraw - -class CircleImageView : AppCompatImageView { - - companion object { - val SCALE_TYPE = ScaleType.CENTER_CROP - } - - private var path: Path? = null - private var rect: RectF? = null - - constructor(context: Context) : this(context, null) - - constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) { - init() - } - - - override fun onDraw(canvas: Canvas) { - rect?.let { rect -> - if (rect.right == 0F || rect.bottom == 0F) { - createRect(width, height) - } - } - - path?.run { canvas.clipPath(this) } - super.onDraw(canvas) - } - - private fun init() { - scaleType = SCALE_TYPE - - doOnPreDraw { createRect(width, height) } - } - - private fun createRect(width: Int, height: Int) { - path = Path() - rect = RectF(0f, 0f, width.toFloat(), height.toFloat()).apply { - path?.addRoundRect( - this, - width.toFloat() / 2F, - height.toFloat() / 2F, - Path.Direction.CW - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/view/DialogToolbar.kt b/app/src/main/kotlin/com/meloda/fast/view/DialogToolbar.kt deleted file mode 100644 index 0508a9f9..00000000 --- a/app/src/main/kotlin/com/meloda/fast/view/DialogToolbar.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.meloda.fast.view - -import android.content.Context -import android.graphics.drawable.Drawable -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.content.ContextCompat -import androidx.core.content.withStyledAttributes -import androidx.core.view.updatePaddingRelative -import com.meloda.fast.R -import com.meloda.fast.databinding.ViewDialogToolbarBinding -import com.meloda.fast.ext.dpToPx -import com.meloda.fast.ext.toggleVisibility -import com.meloda.fast.ext.toggleVisibilityIfHasContent -import com.meloda.fast.util.ColorUtils -import kotlin.properties.Delegates - -class DialogToolbar @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : ConstraintLayout(context, attrs, defStyleAttr) { - - private val binding = - ViewDialogToolbarBinding.inflate(LayoutInflater.from(context), this) - - - var title: String? by Delegates.observable(null) { _, _, _ -> - applyTitle(title) - } - - var subtitle: String? by Delegates.observable(null) { _, _, _ -> - applySubtitle(subtitle) - } - - var avatarDrawable: Drawable? by Delegates.observable(null) { _, _, _ -> - applyAvatarDrawable(avatarDrawable) - } - - var avatarClickAction: ((avatar: View) -> Unit)? by Delegates.observable(null) { _, _, _ -> - applyAvatarClickAction(avatarClickAction) - } - - var startIconDrawable: Drawable? by Delegates.observable(null) { _, _, _ -> - applyStartIconDrawable(startIconDrawable) - } - - var startButtonClickAction: (() -> Unit)? = null - - private val defaultBackgroundColor = ContextCompat.getColor( - context, - R.color.colorBackground - ) - - init { - isSaveEnabled = false - - val padding = 4.dpToPx() - updatePaddingRelative(top = padding, bottom = padding) - - context.withStyledAttributes(attrs, R.styleable.DialogToolbar) { - title = getText(R.styleable.DialogToolbar_title)?.toString() - subtitle = getText(R.styleable.DialogToolbar_subtitle)?.toString() - avatarDrawable = getDrawable(R.styleable.DialogToolbar_avatar) - startIconDrawable = getDrawable(R.styleable.DialogToolbar_startIcon) - - val attrBackgroundColor = - getColor(R.styleable.DialogToolbar_backgroundColor, defaultBackgroundColor) - - val useTranslucentBackgroundColor = - getBoolean(R.styleable.DialogToolbar_useTranslucentBackgroundColor, false) - - val backgroundColor = - if (useTranslucentBackgroundColor) ColorUtils.alphaColor(attrBackgroundColor, 0.9F) - else attrBackgroundColor - - setBackgroundColor(backgroundColor) - } - - binding.startIconContainer.setOnClickListener { startButtonClickAction?.invoke() } - } - - private fun syncView() { - applyTitle(title) - applySubtitle(subtitle) - applyAvatarDrawable(avatarDrawable) - applyStartIconDrawable(startIconDrawable) - } - - private fun applyTitle(title: String?) { - binding.title.text = title - binding.title.toggleVisibilityIfHasContent() - } - - private fun applySubtitle(subtitle: String?) { - binding.subtitle.text = subtitle - binding.subtitle.toggleVisibilityIfHasContent() - } - - private fun applyAvatarDrawable(drawable: Drawable?) { - binding.avatar.setImageDrawable(drawable) - binding.avatar.toggleVisibilityIfHasContent() - } - - private fun applyAvatarClickAction(action: ((avatar: View) -> Unit)?) { - binding.avatar.setOnClickListener(action) - } - - private fun applyStartIconDrawable(drawable: Drawable?) { - binding.startIcon.setImageDrawable(drawable) - - binding.startIconContainer.toggleVisibility(drawable != null) - } - - val avatarImageView get() = binding.avatar - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/view/SpaceItemDecoration.kt b/app/src/main/kotlin/com/meloda/fast/view/SpaceItemDecoration.kt deleted file mode 100644 index 874a7a24..00000000 --- a/app/src/main/kotlin/com/meloda/fast/view/SpaceItemDecoration.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.meloda.fast.view - -import android.graphics.Rect -import android.view.View -import androidx.recyclerview.widget.RecyclerView - -class SpaceItemDecoration( - private val topMargin: Int? = null, - private val endMargin: Int? = null, - private val bottomMargin: Int? = null, - private val startMargin: Int? = null -) : RecyclerView.ItemDecoration() { - - override fun getItemOffsets( - outRect: Rect, - view: View, - parent: RecyclerView, - state: RecyclerView.State - ) { - topMargin?.run { outRect.top = this } - endMargin?.run { outRect.right = this } - bottomMargin?.run { outRect.bottom = this } - startMargin?.run { outRect.left = this } - } - -} \ No newline at end of file diff --git a/app/src/main/res/anim/activity_close_enter.xml b/app/src/main/res/anim/activity_close_enter.xml deleted file mode 100644 index 2651d5c9..00000000 --- a/app/src/main/res/anim/activity_close_enter.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/activity_close_exit.xml b/app/src/main/res/anim/activity_close_exit.xml deleted file mode 100644 index a16e9e5f..00000000 --- a/app/src/main/res/anim/activity_close_exit.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/anim/activity_open_enter.xml b/app/src/main/res/anim/activity_open_enter.xml deleted file mode 100644 index 46e1a064..00000000 --- a/app/src/main/res/anim/activity_open_enter.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/anim/activity_open_exit.xml b/app/src/main/res/anim/activity_open_exit.xml deleted file mode 100644 index faf8690c..00000000 --- a/app/src/main/res/anim/activity_open_exit.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/fast_out_extra_slow_in.xml b/app/src/main/res/anim/fast_out_extra_slow_in.xml deleted file mode 100644 index 241111b6..00000000 --- a/app/src/main/res/anim/fast_out_extra_slow_in.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/app/src/main/res/drawable-hdpi/ic_notification_new_message.png b/app/src/main/res/drawable-hdpi/ic_notification_new_message.png deleted file mode 100644 index 61433bfc10ee4981a04fcd8b81da3a3901813dae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 373 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$UKzz-Z&?;uum9_jc;V+(QlmE}D%& zmmD=3h3+_BVpMfdU(%ezzJ$fggZ*^x8~!ifl;-ZV-GH6@dMtcR};8yM0V2z(^ z(+XDDZ@AF9Z{-w8dzn}KIx6Q}+h@Ymlz+(E@=CwwFXPM^pZ@*Lf0^rgb5Z;m5x0B? zwPk-?-(R@+_-@A3O{QQ4UPo@oPw3QqfB(Pq-nAOmXB5Dy_NZDk0j=16W@_ik#{Na$ z;YN2#{AM;XoZ10)3zT!B^Dkq3o?`9J)KeDBZtzpOH5#p3k94FShLAKe+f s;_=O339(l{{$&1`!o7Xdr=~j$c5&rJ!gnGT1A~*n)78&qol`;+0FhFtqW}N^ diff --git a/app/src/main/res/drawable-mdpi/ic_notification_new_message.png b/app/src/main/res/drawable-mdpi/ic_notification_new_message.png deleted file mode 100644 index aa174d476c595cff716dee6d012bb3d45265fc4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 243 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjyF6VSLn`9lPC3ZiY{26xxm0-r zqnzqu11I+vY#SWIU#O)rU+0rK|7h_9tnqSolW;7TQ|yn=efFJ zYQ{p?1<{2}Y8#BF(D>)^s6;xZ=g{PuKMA zg>0&}+5MBfvm+hoN@0U{l0h@xvh-B9iKJOGKAs2E?zf=e{DkK<2j0pn*xqNpx8t$G z`OgiT?|FVdQ&MBb@0JIa{-v9sr diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification_new_message.png b/app/src/main/res/drawable-xxhdpi/ic_notification_new_message.png deleted file mode 100644 index 6499dce6dcb4929998ce511dbdeb6a2b1dbb0e41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 711 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@Zgyv2VCwU9aSW-5dwc6+;1olFwvXD4 zLOKWgJlLl_nC8HKp|OlXHlnFyf$W;f_QQ#CCO^Mf9J*8Yd-h5DqRM?a&jQ%r6=*D- z66B>B{I2fm_0srw{gT-4rAK$3ThtyMzyHst%(i1c@3}bIsWu+7^nc`kKTPtT<&v5A z{=I!>dEfTD)N|g~g2|?WkM|^YZJ5oFyHN7;eb> zaO3*nM_*qQ9;*atUbJyz*SxcbeI%rTDy<5R|6To($L2~yVLi~fd=lT+FEjoSz^5*{Z8>hflaLRtT7GGcS za_{xE$MxKi%_k0rNqyG5@YwZ#C_d9T=eAmzx9=xphUEjqn_b^t(2Nmy74z)0%h=Y^>bP0 Hl+XkK4g_G? diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml deleted file mode 100644 index 07ad6413..00000000 --- a/app/src/main/res/drawable/ic_back.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_account_circle_24.xml b/app/src/main/res/drawable/ic_baseline_account_circle_24.xml deleted file mode 100644 index 1fc37dc3..00000000 --- a/app/src/main/res/drawable/ic_baseline_account_circle_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_chat_attachment_panel_background.xml b/app/src/main/res/drawable/ic_chat_attachment_panel_background.xml deleted file mode 100644 index 4e6ca60c..00000000 --- a/app/src/main/res/drawable/ic_chat_attachment_panel_background.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_close_in_circle.xml b/app/src/main/res/drawable/ic_close_in_circle.xml deleted file mode 100644 index 96c95cab..00000000 --- a/app/src/main/res/drawable/ic_close_in_circle.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_image_button_circle_background.xml b/app/src/main/res/drawable/ic_image_button_circle_background.xml deleted file mode 100644 index b50a43d5..00000000 --- a/app/src/main/res/drawable/ic_image_button_circle_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_key.xml b/app/src/main/res/drawable/ic_key.xml deleted file mode 100644 index 581f83e0..00000000 --- a/app/src/main/res/drawable/ic_key.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_attachment_story_image_dimmer.xml b/app/src/main/res/drawable/ic_message_attachment_story_image_dimmer.xml deleted file mode 100644 index 35af533c..00000000 --- a/app/src/main/res/drawable/ic_message_attachment_story_image_dimmer.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_in_background.xml b/app/src/main/res/drawable/ic_message_in_background.xml deleted file mode 100644 index e2aa9732..00000000 --- a/app/src/main/res/drawable/ic_message_in_background.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_in_background_middle.xml b/app/src/main/res/drawable/ic_message_in_background_middle.xml deleted file mode 100644 index 2000155e..00000000 --- a/app/src/main/res/drawable/ic_message_in_background_middle.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_out_background.xml b/app/src/main/res/drawable/ic_message_out_background.xml deleted file mode 100644 index 31e9209f..00000000 --- a/app/src/main/res/drawable/ic_message_out_background.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_out_background_middle.xml b/app/src/main/res/drawable/ic_message_out_background_middle.xml deleted file mode 100644 index d1c61162..00000000 --- a/app/src/main/res/drawable/ic_message_out_background_middle.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_out_background_middle_stroke.xml b/app/src/main/res/drawable/ic_message_out_background_middle_stroke.xml deleted file mode 100644 index 463bcc87..00000000 --- a/app/src/main/res/drawable/ic_message_out_background_middle_stroke.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_out_background_stroke.xml b/app/src/main/res/drawable/ic_message_out_background_stroke.xml deleted file mode 100644 index e16a5641..00000000 --- a/app/src/main/res/drawable/ic_message_out_background_stroke.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_panel_background.xml b/app/src/main/res/drawable/ic_message_panel_background.xml deleted file mode 100644 index ff07f7a5..00000000 --- a/app/src/main/res/drawable/ic_message_panel_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_panel_gradient.xml b/app/src/main/res/drawable/ic_message_panel_gradient.xml deleted file mode 100644 index 0b00bdf8..00000000 --- a/app/src/main/res/drawable/ic_message_panel_gradient.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_unread.xml b/app/src/main/res/drawable/ic_message_unread.xml deleted file mode 100644 index 341a6369..00000000 --- a/app/src/main/res/drawable/ic_message_unread.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_messages_history_toolbar_gradient_background.xml b/app/src/main/res/drawable/ic_messages_history_toolbar_gradient_background.xml deleted file mode 100644 index eafd6329..00000000 --- a/app/src/main/res/drawable/ic_messages_history_toolbar_gradient_background.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notification_new_message.xml b/app/src/main/res/drawable/ic_notification_new_message.xml deleted file mode 100644 index 776ab808..00000000 --- a/app/src/main/res/drawable/ic_notification_new_message.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_online_pc.xml b/app/src/main/res/drawable/ic_online_pc.xml deleted file mode 100644 index 1f7e959a..00000000 --- a/app/src/main/res/drawable/ic_online_pc.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_people_outline.xml b/app/src/main/res/drawable/ic_people_outline.xml deleted file mode 100644 index 2f8cbd63..00000000 --- a/app/src/main/res/drawable/ic_people_outline.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_phantom.xml b/app/src/main/res/drawable/ic_phantom.xml deleted file mode 100644 index 3bc9d504..00000000 --- a/app/src/main/res/drawable/ic_phantom.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_play_button_circle_background.xml b/app/src/main/res/drawable/ic_play_button_circle_background.xml deleted file mode 100644 index 96c95cab..00000000 --- a/app/src/main/res/drawable/ic_play_button_circle_background.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_round_access_time_24.xml b/app/src/main/res/drawable/ic_round_access_time_24.xml deleted file mode 100644 index b0aaa17c..00000000 --- a/app/src/main/res/drawable/ic_round_access_time_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_close_20.xml b/app/src/main/res/drawable/ic_round_close_20.xml deleted file mode 100644 index 25b4f3e6..00000000 --- a/app/src/main/res/drawable/ic_round_close_20.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_emoji_emotions_24.xml b/app/src/main/res/drawable/ic_round_emoji_emotions_24.xml deleted file mode 100644 index c8e85353..00000000 --- a/app/src/main/res/drawable/ic_round_emoji_emotions_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_error_outline_24.xml b/app/src/main/res/drawable/ic_round_error_outline_24.xml deleted file mode 100644 index 7d643610..00000000 --- a/app/src/main/res/drawable/ic_round_error_outline_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_group_24.xml b/app/src/main/res/drawable/ic_round_group_24.xml deleted file mode 100644 index 7704f2b3..00000000 --- a/app/src/main/res/drawable/ic_round_group_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_keyboard_arrow_down_24.xml b/app/src/main/res/drawable/ic_round_keyboard_arrow_down_24.xml deleted file mode 100644 index a8340c19..00000000 --- a/app/src/main/res/drawable/ic_round_keyboard_arrow_down_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_link_24.xml b/app/src/main/res/drawable/ic_round_link_24.xml deleted file mode 100644 index a7c819ed..00000000 --- a/app/src/main/res/drawable/ic_round_link_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_mail_24.xml b/app/src/main/res/drawable/ic_round_mail_24.xml deleted file mode 100644 index d9a337d0..00000000 --- a/app/src/main/res/drawable/ic_round_mail_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_mic_24.xml b/app/src/main/res/drawable/ic_round_mic_24.xml deleted file mode 100644 index 57219f6e..00000000 --- a/app/src/main/res/drawable/ic_round_mic_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_play_arrow_24.xml b/app/src/main/res/drawable/ic_round_play_arrow_24.xml deleted file mode 100644 index 78fbfbba..00000000 --- a/app/src/main/res/drawable/ic_round_play_arrow_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_send_24.xml b/app/src/main/res/drawable/ic_round_send_24.xml deleted file mode 100644 index ae931b57..00000000 --- a/app/src/main/res/drawable/ic_round_send_24.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_settings_24.xml b/app/src/main/res/drawable/ic_round_settings_24.xml deleted file mode 100644 index a277a99d..00000000 --- a/app/src/main/res/drawable/ic_round_settings_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_round_settings_primary.xml b/app/src/main/res/drawable/ic_round_settings_primary.xml deleted file mode 100644 index 1d4f7900..00000000 --- a/app/src/main/res/drawable/ic_round_settings_primary.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_round_star_24.xml b/app/src/main/res/drawable/ic_round_star_24.xml deleted file mode 100644 index f62410ba..00000000 --- a/app/src/main/res/drawable/ic_round_star_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml deleted file mode 100644 index 01045afd..00000000 --- a/app/src/main/res/drawable/ic_search.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_security.xml b/app/src/main/res/drawable/ic_security.xml deleted file mode 100644 index ef2e5521..00000000 --- a/app/src/main/res/drawable/ic_security.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_star_border.xml b/app/src/main/res/drawable/ic_star_border.xml deleted file mode 100644 index f341eb01..00000000 --- a/app/src/main/res/drawable/ic_star_border.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/time_read_indicator_on_attachments_background.xml b/app/src/main/res/drawable/time_read_indicator_on_attachments_background.xml deleted file mode 100644 index cf263555..00000000 --- a/app/src/main/res/drawable/time_read_indicator_on_attachments_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/font/tt_commons_bold.ttf b/app/src/main/res/font/tt_commons_bold.ttf deleted file mode 100644 index 98aa0411f45b13438ada81fe828f0387a600b5e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 116140 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 diff --git a/app/src/main/res/font/tt_commons_medium.ttf b/app/src/main/res/font/tt_commons_medium.ttf deleted file mode 100644 index 730b1fa1c950915308e55c94528bc4ea864ee0e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 194384 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

!<{WDdA#Oe=@ZVy76=@t2ox3GHBPO#!0e*{A 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 Kt^3H5?f)Nqxivxn diff --git a/app/src/main/res/font/tt_commons_regular.ttf b/app/src/main/res/font/tt_commons_regular.ttf deleted file mode 100644 index 545705cf70d983410429dade4fa3f10cea34534f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 194848 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 - diff --git a/app/src/main/res/layout/dialog_captcha.xml b/app/src/main/res/layout/dialog_captcha.xml deleted file mode 100644 index 0cb45b0a..00000000 --- a/app/src/main/res/layout/dialog_captcha.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_fast_login.xml b/app/src/main/res/layout/dialog_fast_login.xml deleted file mode 100644 index f20d70b0..00000000 --- a/app/src/main/res/layout/dialog_fast_login.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_message_delete.xml b/app/src/main/res/layout/dialog_message_delete.xml deleted file mode 100644 index a021c392..00000000 --- a/app/src/main/res/layout/dialog_message_delete.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_validation.xml b/app/src/main/res/layout/dialog_validation.xml deleted file mode 100644 index 6f636d65..00000000 --- a/app/src/main/res/layout/dialog_validation.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_header.xml b/app/src/main/res/layout/drawer_header.xml deleted file mode 100644 index 915e4c3f..00000000 --- a/app/src/main/res/layout/drawer_header.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_chat_info.xml b/app/src/main/res/layout/fragment_chat_info.xml deleted file mode 100644 index 963d3e44..00000000 --- a/app/src/main/res/layout/fragment_chat_info.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_chat_info_members.xml b/app/src/main/res/layout/fragment_chat_info_members.xml deleted file mode 100644 index ac3270d3..00000000 --- a/app/src/main/res/layout/fragment_chat_info_members.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_conversations.xml b/app/src/main/res/layout/fragment_conversations.xml deleted file mode 100644 index 1499f534..00000000 --- a/app/src/main/res/layout/fragment_conversations.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_forwarded_messages.xml b/app/src/main/res/layout/fragment_forwarded_messages.xml deleted file mode 100644 index 828374c7..00000000 --- a/app/src/main/res/layout/fragment_forwarded_messages.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_messages_history.xml b/app/src/main/res/layout/fragment_messages_history.xml deleted file mode 100644 index 32435b4d..00000000 --- a/app/src/main/res/layout/fragment_messages_history.xml +++ /dev/null @@ -1,297 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml deleted file mode 100644 index bca1d865..00000000 --- a/app/src/main/res/layout/fragment_settings.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_updates.xml b/app/src/main/res/layout/fragment_updates.xml deleted file mode 100644 index 20cfcb9e..00000000 --- a/app/src/main/res/layout/fragment_updates.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_user_banned.xml b/app/src/main/res/layout/fragment_user_banned.xml deleted file mode 100644 index c0b90365..00000000 --- a/app/src/main/res/layout/fragment_user_banned.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_chat_member.xml b/app/src/main/res/layout/item_chat_member.xml deleted file mode 100644 index 4b6d30ff..00000000 --- a/app/src/main/res/layout/item_chat_member.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_conversation.xml b/app/src/main/res/layout/item_conversation.xml deleted file mode 100644 index 00c71acf..00000000 --- a/app/src/main/res/layout/item_conversation.xml +++ /dev/null @@ -1,256 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_audio.xml b/app/src/main/res/layout/item_message_attachment_audio.xml deleted file mode 100644 index eccc8c89..00000000 --- a/app/src/main/res/layout/item_message_attachment_audio.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_message_attachment_call.xml b/app/src/main/res/layout/item_message_attachment_call.xml deleted file mode 100644 index 750f452f..00000000 --- a/app/src/main/res/layout/item_message_attachment_call.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_file.xml b/app/src/main/res/layout/item_message_attachment_file.xml deleted file mode 100644 index 0d83ba32..00000000 --- a/app/src/main/res/layout/item_message_attachment_file.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_forwards.xml b/app/src/main/res/layout/item_message_attachment_forwards.xml deleted file mode 100644 index bb0f2f9a..00000000 --- a/app/src/main/res/layout/item_message_attachment_forwards.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_geo.xml b/app/src/main/res/layout/item_message_attachment_geo.xml deleted file mode 100644 index 950b9f77..00000000 --- a/app/src/main/res/layout/item_message_attachment_geo.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_gift.xml b/app/src/main/res/layout/item_message_attachment_gift.xml deleted file mode 100644 index 8b89d013..00000000 --- a/app/src/main/res/layout/item_message_attachment_gift.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_graffiti.xml b/app/src/main/res/layout/item_message_attachment_graffiti.xml deleted file mode 100644 index 8b89d013..00000000 --- a/app/src/main/res/layout/item_message_attachment_graffiti.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_link.xml b/app/src/main/res/layout/item_message_attachment_link.xml deleted file mode 100644 index 584e5008..00000000 --- a/app/src/main/res/layout/item_message_attachment_link.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_photo.xml b/app/src/main/res/layout/item_message_attachment_photo.xml deleted file mode 100644 index 97f91bbd..00000000 --- a/app/src/main/res/layout/item_message_attachment_photo.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_reply.xml b/app/src/main/res/layout/item_message_attachment_reply.xml deleted file mode 100644 index 0181df75..00000000 --- a/app/src/main/res/layout/item_message_attachment_reply.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_sticker.xml b/app/src/main/res/layout/item_message_attachment_sticker.xml deleted file mode 100644 index 56adcaca..00000000 --- a/app/src/main/res/layout/item_message_attachment_sticker.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_story.xml b/app/src/main/res/layout/item_message_attachment_story.xml deleted file mode 100644 index 7698f47e..00000000 --- a/app/src/main/res/layout/item_message_attachment_story.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_video.xml b/app/src/main/res/layout/item_message_attachment_video.xml deleted file mode 100644 index 1829dd48..00000000 --- a/app/src/main/res/layout/item_message_attachment_video.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_voice.xml b/app/src/main/res/layout/item_message_attachment_voice.xml deleted file mode 100644 index 4532d531..00000000 --- a/app/src/main/res/layout/item_message_attachment_voice.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_wall_post.xml b/app/src/main/res/layout/item_message_attachment_wall_post.xml deleted file mode 100644 index a4e0a877..00000000 --- a/app/src/main/res/layout/item_message_attachment_wall_post.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_in.xml b/app/src/main/res/layout/item_message_in.xml deleted file mode 100644 index f955d8d9..00000000 --- a/app/src/main/res/layout/item_message_in.xml +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_out.xml b/app/src/main/res/layout/item_message_out.xml deleted file mode 100644 index 95c03742..00000000 --- a/app/src/main/res/layout/item_message_out.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_service.xml b/app/src/main/res/layout/item_message_service.xml deleted file mode 100644 index dc0dd047..00000000 --- a/app/src/main/res/layout/item_message_service.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_settings_checkbox.xml b/app/src/main/res/layout/item_settings_checkbox.xml deleted file mode 100644 index d3959648..00000000 --- a/app/src/main/res/layout/item_settings_checkbox.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_settings_edit_text.xml b/app/src/main/res/layout/item_settings_edit_text.xml deleted file mode 100644 index 06331356..00000000 --- a/app/src/main/res/layout/item_settings_edit_text.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_settings_edit_text_alert.xml b/app/src/main/res/layout/item_settings_edit_text_alert.xml deleted file mode 100644 index 6a23eae5..00000000 --- a/app/src/main/res/layout/item_settings_edit_text_alert.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/layout/item_settings_list.xml b/app/src/main/res/layout/item_settings_list.xml deleted file mode 100644 index 9106ecae..00000000 --- a/app/src/main/res/layout/item_settings_list.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/item_settings_switch.xml b/app/src/main/res/layout/item_settings_switch.xml deleted file mode 100644 index 2328e3db..00000000 --- a/app/src/main/res/layout/item_settings_switch.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout/item_settings_title.xml b/app/src/main/res/layout/item_settings_title.xml deleted file mode 100644 index 54a00174..00000000 --- a/app/src/main/res/layout/item_settings_title.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_settings_title_summary.xml b/app/src/main/res/layout/item_settings_title_summary.xml deleted file mode 100644 index 06331356..00000000 --- a/app/src/main/res/layout/item_settings_title_summary.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_uploaded_attachment_audio.xml b/app/src/main/res/layout/item_uploaded_attachment_audio.xml deleted file mode 100644 index a85ffdca..00000000 --- a/app/src/main/res/layout/item_uploaded_attachment_audio.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_uploaded_attachment_file.xml b/app/src/main/res/layout/item_uploaded_attachment_file.xml deleted file mode 100644 index b5fd2bab..00000000 --- a/app/src/main/res/layout/item_uploaded_attachment_file.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_uploaded_attachment_photo.xml b/app/src/main/res/layout/item_uploaded_attachment_photo.xml deleted file mode 100644 index 1e02c66e..00000000 --- a/app/src/main/res/layout/item_uploaded_attachment_photo.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_uploaded_attachment_video.xml b/app/src/main/res/layout/item_uploaded_attachment_video.xml deleted file mode 100644 index 6a36733e..00000000 --- a/app/src/main/res/layout/item_uploaded_attachment_video.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/toolbar_menu_item_avatar.xml b/app/src/main/res/layout/toolbar_menu_item_avatar.xml deleted file mode 100644 index 68ca49af..00000000 --- a/app/src/main/res/layout/toolbar_menu_item_avatar.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/view_dialog_toolbar.xml b/app/src/main/res/layout/view_dialog_toolbar.xml deleted file mode 100644 index a99cca08..00000000 --- a/app/src/main/res/layout/view_dialog_toolbar.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/activity_main_bottom.xml b/app/src/main/res/menu/activity_main_bottom.xml deleted file mode 100644 index 36b364d7..00000000 --- a/app/src/main/res/menu/activity_main_bottom.xml +++ /dev/null @@ -1,19 +0,0 @@ - -

- - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/activity_main_drawer.xml b/app/src/main/res/menu/activity_main_drawer.xml deleted file mode 100644 index 3da21556..00000000 --- a/app/src/main/res/menu/activity_main_drawer.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/fragment_conversations.xml b/app/src/main/res/menu/fragment_conversations.xml deleted file mode 100644 index 167638f2..00000000 --- a/app/src/main/res/menu/fragment_conversations.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/menu/fragment_conversations_popup.xml b/app/src/main/res/menu/fragment_conversations_popup.xml deleted file mode 100644 index b05248ac..00000000 --- a/app/src/main/res/menu/fragment_conversations_popup.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/values-night/bools.xml b/app/src/main/res/values-night/bools.xml deleted file mode 100644 index d1af27f2..00000000 --- a/app/src/main/res/values-night/bools.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - false - false - - \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml deleted file mode 100644 index 6f14ead9..00000000 --- a/app/src/main/res/values-night/colors.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - #40000000 - diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml deleted file mode 100644 index d102c852..00000000 --- a/app/src/main/res/values-ru/strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - Вложения - Настроечбки - Настроечбки - diff --git a/app/src/main/res/values-v27/themes.xml b/app/src/main/res/values-v27/themes.xml deleted file mode 100644 index 6646f962..00000000 --- a/app/src/main/res/values-v27/themes.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml deleted file mode 100644 index 9516afa1..00000000 --- a/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index d10e89d9..c37be342 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -2,4 +2,5 @@ + diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 00000000..ed8839d4 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml deleted file mode 100644 index d8cf32d5..00000000 --- a/app/src/main/res/xml/preferences.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml deleted file mode 100644 index 381211ff..00000000 --- a/app/src/main/res/xml/shortcuts.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - diff --git a/app/src/staging/res/mipmap-hdpi/ic_launcher.png b/app/src/staging/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..198f18ea30034e91156bcb4c68ae90d0b07636d3 GIT binary patch literal 3284 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 SU77{}0000GHjVYDwbj@ojkT#g)g~s|rbXH& z)&?x4ZA`6ggRxrTQAE4~9_+G6jaEQg51LjYFn!;fdBg0R0d|JPKl!r5&j0@R{qOz% z|Bl7aZ@?n@+wJ0m%dNEm_~hU6;;Ae8^Y8DQZ1fW!NKQU9g#R95Pd*n|XlT7XSUAi5~GKf_rMA&Hb#y=H5}5==#lJbGZmw9YW(x3OtkVq<2Jz=%S6< z#(i-FQ_r3;q6N05B0{XPz}DFAur-PWHkSdQ4cayp6~(kXq}PWJMT&Fw>=4{5^KGsM z57L6G+mYz%@&?dVJ40;|n=B`!8TL_rs1vg&>5`3%S~Vk1!lNtS=Ddeb?Fjb`y|xaU zvl@Nq^=(Sj21x9#CFo?GlMtZKzCmLra|C^mL0YTF!H}pMk}3a`jXFk+4*{Qm*rc}Q zza7HG9eP~65_L6j2(B`a#|Y8*O(@V@FyKT_@)QWpVoF|R)CksP zHH%+tY898KG-=-T1@&O%Ce)Ys$xe8c*sR7E*qm#WkvBnp*?hP7-Dfu_%0IcTdEXzD zeg&s!x1L(3=GLpQ8eEXjASelIGHgn>wJ8n&{CjUeXPyjVzHqTKJNDR2HAju8<33+- zR>Ocgsf^%*7p{1aH|qevIM@@;)DUWvqwb2$riYMU!9vGOwOKmPEq+_#Nq!&>&;=4} zhR`5p#UEd-=BfzuXu0*zkwmdUYHY5~mu>e99RLpTdd5K32{Nd5>uKl0(9%cLoT)I6 z@)GKbl+o81=^yW{(uNLDS#eXNYZTHfTof~74lPx4_EVRpJmEqml+DJzUg%U5-w;n! zwQF>Zf^o`M)|z4ka@)fKthmheOrB-tmb8w5;0PWL>ChPmA1b z0cXmcl_h(^*`;*5U?Oo}&HY0|9{6F`UzEHr9FWl1jWBvD=D;d&rrg;KCwNG63zjXyS$a~&xrncHC4n`!o+rh2Tz#C=9jK&+Vly^ ziEJejFNpKQzj_&*J+=}vLbt||M7|AWGn+XcJYhB3u6Ahj`-On`cjIJsCqDSKBvte% z+v&h8>xqL+jEI_qW1G~_+>w%jCk|X`x~tLi4$_e_Gw!Hs)bQ|N zHD^U+kA|m2maV6=KoVvkv%r(@?CcU(rW(88qVOF3x4=cmpN&ryn>)xIF=acE1Y!Yew0#Vb2s(*@;o}!gs!pnv3Z}3DYs&C$3-XP#kz}zIXe54@hORJ2$ajoI9;} z3pnY9j#f~b?3kj7X_h@@ID-;Hxa2bA#0nDc*zmov+AhLXpLmWeC!Gh+7FO0&_ZcEs34u(Z-kKk^Y9umzhLF_pEE6P7v97y2|D zHoc4JZ?SBI6SxV8m=m`Boy^!OC#D*NFzpmLaCie*A8la+wq}QIdzaXbL?2RLUM8#& zk!vtQYJ5PzsJNui&Fj;n_f=(EYj0r$r4<|w(b?f39~+4dR#m(8O3wqO&s z`>pc>7tvqtWCX!Df@xF6rl-#e+xli|WJP(#^!?R~UlIL5!fvS;D(4BC9sK{p!!q!U?*gEMla%EAz~lUX6bf^1B-qF+00000 LNkvXXu0mjfSp(H~ literal 0 HcmV?d00001 diff --git a/app/src/staging/res/mipmap-xhdpi/ic_launcher.png b/app/src/staging/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..37a31bb61188180dae9f837eb8827abdfcaa037c GIT binary patch literal 4572 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 + + Fast Staging + diff --git a/build.gradle.kts b/build.gradle.kts index 58a69141..c8cdbe3b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,28 +1,28 @@ -buildscript { - - repositories { - google() - mavenCentral() - } - dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10") - classpath("com.android.tools.build:gradle:8.1.0") - } -} +import dev.iurysouza.modulegraph.Theme plugins { - id("org.jetbrains.kotlin.android") version "1.8.20" apply false - id("com.google.devtools.ksp") version "1.8.20-1.0.11" apply false + alias(libs.plugins.com.android.application) apply false + alias(libs.plugins.org.jetbrains.kotlin.android) apply false + alias(libs.plugins.org.jetbrains.kotlin.plugin.parcelize) apply false + alias(libs.plugins.android.library) apply false + + id("dev.iurysouza.modulegraph") version "0.8.1" } -allprojects { - repositories { - google() - mavenCentral() - maven(url = "https://jitpack.io") - } -} - -tasks.register("clean", Delete::class) { - delete(rootProject.buildDir) +moduleGraphConfig { + readmePath.set("${rootDir}/README.md") + heading.set("### Module Graph") + theme.set( + Theme.BASE( + mapOf( + "primaryTextColor" to "#fff", + "primaryColor" to "#5a4f7c", + "primaryBorderColor" to "#5a4f7c", + "lineColor" to "#f5a623", + "tertiaryColor" to "#40375c", + "fontSize" to "12px", + ), + focusColor = "#FA8140" + ), + ) } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..b6413e30 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + `kotlin-dsl` +} + +repositories { + mavenCentral() +} diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 00000000..29744ec1 --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "buildSrc" diff --git a/buildSrc/src/main/kotlin/Configs.kt b/buildSrc/src/main/kotlin/Configs.kt new file mode 100644 index 00000000..ca8e7c9a --- /dev/null +++ b/buildSrc/src/main/kotlin/Configs.kt @@ -0,0 +1,13 @@ +import org.gradle.api.JavaVersion + +object Configs { + + const val appCode = 1 + const val appName = "1.8.1" + + const val compileSdk = 34 + const val minSdk = 24 + const val targetSdk = 34 + + val java = JavaVersion.VERSION_17 +} diff --git a/core/common/.gitignore b/core/common/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/common/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts new file mode 100644 index 00000000..394009eb --- /dev/null +++ b/core/common/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.org.jetbrains.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.org.jetbrains.kotlin.plugin.parcelize) + alias(libs.plugins.kotlin.compose.compiler) +} + +group = "com.meloda.app.fast.common" + +android { + namespace = "com.meloda.app.fast.common" + compileSdk = Configs.compileSdk + + defaultConfig { + minSdk = Configs.minSdk + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = Configs.java + targetCompatibility = Configs.java + } + kotlinOptions { + jvmTarget = Configs.java.toString() + freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xcontext-receivers") + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(libs.core.ktx) + implementation(libs.preference.ktx) + + implementation(libs.lifecycle.viewmodel.ktx) + implementation(libs.lifecycle.runtime.ktx) + + implementation(libs.koin.android) + implementation(libs.koin.androidx.compose) + implementation(libs.koin.androidx.compose.navigation) + + implementation(libs.coil.compose) + + implementation(libs.nanokt.jvm) + implementation(libs.nanokt.android) + implementation(libs.nanokt) + + implementation(libs.androidx.navigation.compose) + + implementation(libs.kotlin.serialization) +} diff --git a/core/common/src/main/AndroidManifest.xml b/core/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/core/common/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/AppConstants.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/AppConstants.kt new file mode 100644 index 00000000..225c576e --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/AppConstants.kt @@ -0,0 +1,10 @@ +package com.meloda.app.fast.common + +object AppConstants { + + const val INSTALL_APP_MIME_TYPE = "application/vnd.android.package-archive" + + const val API_VERSION = "5.173" + const val URL_OAUTH = "https://oauth.vk.com" + const val URL_API = "https://api.vk.com/method" +} diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/AuthInterceptor.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/AuthInterceptor.kt new file mode 100644 index 00000000..74a61c10 --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/AuthInterceptor.kt @@ -0,0 +1,31 @@ +package com.meloda.app.fast.common + +import androidx.core.net.toUri +import okhttp3.Interceptor +import okhttp3.Response +import java.net.URLEncoder + +class AuthInterceptor : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val builder = chain.request().url.newBuilder() + + val uri = builder.build().toUri().toString().toUri() + + if (uri.getQueryParameter("v") == null) { + builder.addQueryParameter( + name = "v", + value = URLEncoder.encode(AppConstants.API_VERSION, "utf-8") + ) + } + + if (UserConfig.accessToken.isNotBlank()) { + builder.addQueryParameter( + "access_token", + URLEncoder.encode(UserConfig.accessToken, "utf-8") + ) + } + + return chain.proceed(chain.request().newBuilder().apply { url(builder.build()) }.build()) + } +} diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/CustomNavType.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/CustomNavType.kt new file mode 100644 index 00000000..23d7af9a --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/CustomNavType.kt @@ -0,0 +1,22 @@ +package com.meloda.app.fast.common + +import android.os.Bundle +import android.os.Parcelable +import androidx.core.os.BundleCompat +import androidx.navigation.NavType +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +inline fun customNavType( + isNullableAllowed: Boolean = false, + json: Json = Json +) = object : NavType(isNullableAllowed = isNullableAllowed) { + override fun get(bundle: Bundle, key: String) = + BundleCompat.getParcelable(bundle, key, T::class.java) + + override fun parseValue(value: String): T = json.decodeFromString(value) + + override fun serializeAsValue(value: T): String = json.encodeToString(value) + + override fun put(bundle: Bundle, key: String, value: T) = bundle.putParcelable(key, value) +} diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/UiImage.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/UiImage.kt new file mode 100644 index 00000000..709f50cd --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/UiImage.kt @@ -0,0 +1,29 @@ +package com.meloda.app.fast.common + +import android.graphics.drawable.Drawable +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes + +sealed class UiImage { + + data class Resource(@DrawableRes val resId: Int) : UiImage() + + data class Simple(val drawable: Drawable) : UiImage() + + data class Color(@ColorInt val color: Int) : UiImage() + + data class ColorResource(@ColorRes val resId: Int) : UiImage() + + data class Url(val url: String) : UiImage() + + fun extractUrl(): String? = when (this) { + is Url -> this.url + else -> null + } + + fun extractResId(): Int = when (this) { + is Resource -> this.resId + else -> throw IllegalStateException("this UiImage is not Resource") + } +} diff --git a/app/src/main/kotlin/com/meloda/fast/model/base/UiText.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/UiText.kt similarity index 52% rename from app/src/main/kotlin/com/meloda/fast/model/base/UiText.kt rename to core/common/src/main/kotlin/com/meloda/app/fast/common/UiText.kt index 6fdcac24..0f06da12 100644 --- a/app/src/main/kotlin/com/meloda/fast/model/base/UiText.kt +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/UiText.kt @@ -1,20 +1,18 @@ -package com.meloda.fast.model.base +package com.meloda.app.fast.common -import android.content.Context -import android.os.Parcelable +import android.content.res.Resources import androidx.annotation.PluralsRes import androidx.annotation.StringRes -import kotlinx.parcelize.Parcelize -import kotlinx.parcelize.RawValue -@Parcelize -sealed class UiText : Parcelable { +sealed class UiText { + + data object Empty : UiText() data class Resource(@StringRes val resId: Int) : UiText() data class ResourceParams( @StringRes val value: Int, - val args: List<@RawValue Any?>, + val args: List, ) : UiText() data class Simple(val text: String) : UiText() @@ -22,20 +20,20 @@ sealed class UiText : Parcelable { data class QuantityResource(@PluralsRes val resId: Int, val quantity: Int) : UiText() } -fun UiText?.parseString(context: Context): String? { +fun UiText?.parseString(resources: Resources): String? { return when (this) { - is UiText.Resource -> context.getString(resId) + is UiText.Resource -> resources.getString(resId) is UiText.ResourceParams -> { val processedArgs = args.map { any -> when (any) { - is UiText -> any.parseString(context) + is UiText -> any.parseString(resources) else -> any } } - context.getString(value, *processedArgs.toTypedArray()) + resources.getString(value, *processedArgs.toTypedArray()) } - is UiText.QuantityResource -> context.resources.getQuantityString(resId, quantity, quantity) + is UiText.QuantityResource -> resources.getQuantityString(resId, quantity, quantity) is UiText.Simple -> text else -> null } diff --git a/app/src/main/kotlin/com/meloda/fast/api/UserConfig.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/UserConfig.kt similarity index 56% rename from app/src/main/kotlin/com/meloda/fast/api/UserConfig.kt rename to core/common/src/main/kotlin/com/meloda/app/fast/common/UserConfig.kt index e0d69f7c..490a615b 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/UserConfig.kt +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/UserConfig.kt @@ -1,18 +1,18 @@ -package com.meloda.fast.api +package com.meloda.app.fast.common +import android.content.SharedPreferences import androidx.core.content.edit -import com.meloda.fast.api.model.VkUser -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.model.AppAccount -import kotlinx.coroutines.flow.MutableStateFlow +import kotlin.properties.Delegates object UserConfig { private const val ARG_CURRENT_USER_ID = "current_user_id" - const val FAST_APP_ID = "6964679" + private var preferences: SharedPreferences by Delegates.notNull() - private val preferences get() = AppGlobal.preferences + fun init(preferences: SharedPreferences) { + this.preferences = preferences + } var currentUserId: Int = -1 get() = preferences.getInt(ARG_CURRENT_USER_ID, -1) @@ -24,12 +24,7 @@ object UserConfig { var userId: Int = -1 var accessToken: String = "" var fastToken: String? = "" - - fun parse(account: AppAccount) { - this.userId = account.userId - this.accessToken = account.accessToken - this.fastToken = account.fastToken - } + var trustedHash: String? = null fun clear() { currentUserId = -1 @@ -41,7 +36,4 @@ object UserConfig { fun isLoggedIn(): Boolean { return currentUserId > 0 && userId > 0 && accessToken.isNotBlank() } - - val vkUser: MutableStateFlow = MutableStateFlow(null) - } diff --git a/app/src/main/kotlin/com/meloda/fast/api/VKConstants.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/VkConstants.kt similarity index 61% rename from app/src/main/kotlin/com/meloda/fast/api/VKConstants.kt rename to core/common/src/main/kotlin/com/meloda/app/fast/common/VkConstants.kt index e675fe2b..d126da24 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/VKConstants.kt +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/VkConstants.kt @@ -1,18 +1,15 @@ -package com.meloda.fast.api +package com.meloda.app.fast.common -import com.meloda.fast.api.model.attachments.* - -@Suppress("RemoveExplicitTypeArguments") -object VKConstants { +object VkConstants { const val GROUP_FIELDS = "description,members_count,counters,status,verified" const val USER_FIELDS = "photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex,online_info,bdate" - const val ALL_FIELDS = "$USER_FIELDS,$GROUP_FIELDS" + const val ALL_FIELDS = + "$USER_FIELDS,$GROUP_FIELDS" - const val API_VERSION = "5.173" const val LP_VERSION = 10 const val VK_APP_ID = "2274003" @@ -42,15 +39,15 @@ object VKConstants { } } - val restrictedToEditAttachments = listOf>( - VkCall::class.java, - VkCurator::class.java, - VkEvent::class.java, - VkGift::class.java, - VkGraffiti::class.java, - VkGroupCall::class.java, - VkStory::class.java, - VkVoiceMessage::class.java, - VkWidget::class.java - ) +// val restrictedToEditAttachments = listOf>( +// VkCallDomain::class.java, +// VkCuratorDomain::class.java, +// VkEventDomain::class.java, +// VkGiftDomain::class.java, +// VkGraffitiDomain::class.java, +// VkGroupCallDomain::class.java, +// VkStoryDomain::class.java, +// VkAudioMessageDomain::class.java, +// VkWidgetDomain::class.java +// ) } diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/di/CommonModule.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/di/CommonModule.kt new file mode 100644 index 00000000..633eaa09 --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/di/CommonModule.kt @@ -0,0 +1,12 @@ +package com.meloda.app.fast.common.di + +import coil.ImageLoader +import org.koin.dsl.module + +val commonModule = module { + single { + ImageLoader.Builder(get()) + .crossfade(true) + .build() + } +} diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/Extensions.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/Extensions.kt new file mode 100644 index 00000000..0b8b2a8a --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/Extensions.kt @@ -0,0 +1,151 @@ +package com.meloda.app.fast.common.extensions + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds + +fun Context.restartApp() { + (this as? Activity)?.let { activity -> + activity.finishAffinity() + activity.startActivity( + Intent( + this, + Class.forName("com.meloda.app.fast.MainActivity") + ) + ) + } +} + +inline fun Iterable.findWithIndex(predicate: (T) -> Boolean): Pair? { + val value = firstOrNull(predicate) ?: return null + return indexOf(value).let { index -> if (index == -1) null else index to value } +} + +fun MutableList.addIf(element: T, condition: () -> Boolean) { + if (condition.invoke()) add(element) +} + +context(ViewModel) +fun Flow.listenValue(action: suspend (T) -> Unit) = listenValue(viewModelScope, action) + +fun Flow.listenValue( + coroutineScope: CoroutineScope, + action: suspend (T) -> Unit +): Job = onEach(action::invoke).launchIn(coroutineScope) + +fun createTimerFlow( + time: Int, + onStartAction: (suspend () -> Unit)? = null, + onTickAction: (suspend (remainedTime: Int) -> Unit)? = null, + onTimeoutAction: (suspend () -> Unit)? = null, + interval: Duration = 1.seconds +): Flow = (time downTo 0) + .asSequence() + .asFlow() + .onStart { onStartAction?.invoke() } + .onEach { timeLeft -> + onTickAction?.invoke(timeLeft) + if (timeLeft == 0) { + onTimeoutAction?.invoke() + } else { + delay(interval) + } + } + +fun createTimerFlow( + isNeedToEndCondition: suspend () -> Boolean, + onStartAction: (suspend () -> Unit)? = null, + onTickAction: (suspend () -> Unit)? = null, + onEndAction: (suspend () -> Unit)? = null, + interval: Duration = 1.seconds +): Flow = flow { + while (true) { + val isNeedToEnd = isNeedToEndCondition() + emit(isNeedToEnd) + if (isNeedToEnd) break + } +} + .onStart { onStartAction?.invoke() } + .onEach { isNeedToEnd -> + onTickAction?.invoke() + if (isNeedToEnd) { + onEndAction?.invoke() + } else { + delay(interval) + } + } + +context(ViewModel) +fun MutableSharedFlow.emitOnMainScope(value: T) = emitOnScope(Dispatchers.Main) { value } + +context(ViewModel) +fun MutableSharedFlow.emitOnScope( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + value: () -> T, +) { + viewModelScope.launch(coroutineContext) { + emit(value()) + } +} + +context(CoroutineScope) +suspend fun MutableSharedFlow.emitWithMain(value: T) { + withContext(Dispatchers.Main) { + emit(value) + } +} + +context(ViewModel) +fun MutableStateFlow.updateValue(newValue: T) = this.update { newValue } + +fun MutableStateFlow.setValue(function: (T) -> T) { + val newValue = function(value) + update { newValue } +} + +fun Any.asInt(): Int { + return when (this) { + is Number -> this.toInt() + + else -> throw IllegalArgumentException("Object is not numeric") + } +} + +fun Any.toList(mapper: (old: Any) -> T): List { + return when (this) { + is List<*> -> this.mapNotNull { it?.run(mapper) } + + else -> emptyList() + } +} + +fun isSdkAtLeast(sdkInt: Int, action: (() -> Unit)? = null): Boolean { + return if (Build.VERSION.SDK_INT >= sdkInt) { + action?.invoke() + true + } else { + false + } +} diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/StringsExtensions.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/StringsExtensions.kt new file mode 100644 index 00000000..e547bc87 --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/StringsExtensions.kt @@ -0,0 +1,17 @@ +package com.meloda.app.fast.common.extensions + +inline fun String?.ifEmpty(defaultValue: () -> String?): String? = + if (this?.isEmpty() == true) defaultValue() else this + +fun String?.orDots(count: Int = 3): String { + return this ?: ("." * count) +} + +operator fun String.times(count: Int): String { + val builder = StringBuilder() + for (i in 0 until count) { + builder.append(this) + } + + return builder.toString() +} diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/navigation/NavigationExtensions.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/navigation/NavigationExtensions.kt new file mode 100644 index 00000000..c5a7ad42 --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/extensions/navigation/NavigationExtensions.kt @@ -0,0 +1,18 @@ +package com.meloda.app.fast.common.extensions.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.lifecycle.ViewModel +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavController +import org.koin.androidx.compose.koinViewModel +import org.koin.androidx.compose.navigation.koinNavViewModel + +@Composable +inline fun NavBackStackEntry.sharedViewModel(navController: NavController): T { + val navGraphRoute = destination.parent?.route ?: return koinViewModel() + val parentEntry = remember(this) { + navController.getBackStackEntry(navGraphRoute) + } + return koinNavViewModel(viewModelStoreOwner = parentEntry) +} diff --git a/app/src/main/kotlin/com/meloda/fast/util/AndroidUtils.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/util/AndroidUtils.kt similarity index 72% rename from app/src/main/kotlin/com/meloda/fast/util/AndroidUtils.kt rename to core/common/src/main/kotlin/com/meloda/app/fast/common/util/AndroidUtils.kt index 83c34eee..43c11b0c 100644 --- a/app/src/main/kotlin/com/meloda/fast/util/AndroidUtils.kt +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/util/AndroidUtils.kt @@ -1,82 +1,58 @@ -package com.meloda.fast.util +package com.meloda.app.fast.common.util import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent -import android.content.res.Resources import android.graphics.Bitmap import android.net.Uri import android.os.Build import android.os.PowerManager import android.provider.Settings -import android.util.TypedValue import android.widget.Toast -import androidx.annotation.AttrRes import androidx.core.content.FileProvider -import androidx.core.graphics.Insets -import androidx.core.view.WindowInsetsCompat -import com.meloda.fast.BuildConfig -import com.meloda.fast.common.AppGlobal -import com.meloda.fast.ext.isTrue import java.io.File import java.io.FileOutputStream +private object BuildConfig { + const val DEBUG = true + const val APPLICATION_ID = "com.meloda.app.fast" +} object AndroidUtils { - fun getDisplayWidth(): Int { - return Resources.getSystem().displayMetrics.widthPixels - } - - fun getDisplayHeight(): Int { - return Resources.getSystem().displayMetrics.heightPixels - } - fun copyText( + context: Context, label: String? = "", text: String, withToast: Boolean = false ) { val clipboardManager = - AppGlobal.Instance.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager clipboardManager.setPrimaryClip(ClipData.newPlainText(label, text)) if (withToast && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { - Toast.makeText(AppGlobal.Instance, "Copied to clipboard", Toast.LENGTH_SHORT).show() + Toast.makeText(context, "Copied to clipboard", Toast.LENGTH_SHORT).show() } } fun copyImage( + context: Context, label: String? = "", imageUri: Uri, withToast: Boolean = false ) { val clipboardManager = - AppGlobal.Instance.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager clipboardManager.setPrimaryClip(ClipData.newRawUri(label, imageUri)) if (withToast && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { - Toast.makeText(AppGlobal.Instance, "Copied to clipboard", Toast.LENGTH_SHORT).show() + Toast.makeText(context, "Copied to clipboard", Toast.LENGTH_SHORT).show() } } - fun getThemeAttrColor(context: Context, @AttrRes resId: Int): Int { - val typedValue = TypedValue() - context.theme.resolveAttribute(resId, typedValue, true) - val colorRes = typedValue.resourceId - var color = -1 - try { - color = context.resources.getColor(colorRes, context.theme) - } catch (e: Exception) { - e.printStackTrace() - } - - return color - } - fun bytesToMegabytes(bytes: Double): Double { return bytes / 1024 / 1024 } @@ -88,15 +64,30 @@ object AndroidUtils { else -> "$bytes B" } + fun openAppNotificationsSettings(context: Context) { + val packageName = context.packageName + + val intent = Intent("android.settings.APP_NOTIFICATION_SETTINGS") + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + intent.putExtra("android.provider.extra.APP_PACKAGE", packageName) + } else { + intent.putExtra("app_package", packageName) + intent.putExtra("app_uid", context.applicationInfo.uid) + } + context.startActivity(intent) + } + @Suppress("DEPRECATION") - fun isCanInstallUnknownApps(): Boolean { + fun isCanInstallUnknownApps(context: Context): Boolean { return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { Settings.Secure.getInt( - AppGlobal.Instance.contentResolver, + context.contentResolver, Settings.Secure.INSTALL_NON_MARKET_APPS ) == 1 } else { - AppGlobal.packageManager.canRequestPackageInstalls() + context.packageManager.canRequestPackageInstalls() } } @@ -130,20 +121,8 @@ object AndroidUtils { return intent } - fun getStatusBarInsets(insets: WindowInsetsCompat): Insets { - return insets.getInsets(WindowInsetsCompat.Type.statusBars()) - } - - fun getNavBarInsets(insets: WindowInsetsCompat): Insets { - return insets.getInsets(WindowInsetsCompat.Type.navigationBars()) - } - - fun getImeInsets(insets: WindowInsetsCompat): Insets { - return insets.getInsets(WindowInsetsCompat.Type.ime()) - } - - fun isBatterySaverOn(): Boolean { - return (AppGlobal.Instance.getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isPowerSaveMode.isTrue + fun isBatterySaverOn(context: Context): Boolean { + return (context.getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isPowerSaveMode == true } fun getImageToShare(context: Context, existingFile: File): Uri? { @@ -226,3 +205,4 @@ sealed class ShareContent { data class TextWithImage(val text: String, val imageUri: Uri) : ShareContent() } + diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/util/TimeUtils.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/util/TimeUtils.kt new file mode 100644 index 00000000..468c439e --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/util/TimeUtils.kt @@ -0,0 +1,83 @@ +package com.meloda.app.fast.common.util + +import android.content.res.Resources +import com.conena.nanokt.jvm.util.dayOfMonth +import com.conena.nanokt.jvm.util.hour +import com.conena.nanokt.jvm.util.hourOfDay +import com.conena.nanokt.jvm.util.millisecond +import com.conena.nanokt.jvm.util.minute +import com.conena.nanokt.jvm.util.month +import com.conena.nanokt.jvm.util.second +import com.conena.nanokt.jvm.util.year +import com.meloda.app.fast.common.R +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale + +object TimeUtils { + + fun removeTime(date: Date): Long { + return Calendar.getInstance().apply { + time = date + hourOfDay = 0 + minute = 0 + second = 0 + millisecond = 0 + }.timeInMillis + } + + fun getLocalizedDate(resources: Resources, date: Long): String { + val now = Calendar.getInstance() + val then = Calendar.getInstance().also { it.timeInMillis = date } + + val pattern = when { + now.year != then.year -> "dd MMM yyyy" + now.month != then.month -> "dd MMMM" + now.dayOfMonth != then.dayOfMonth -> { + if (now.dayOfMonth - then.dayOfMonth == 1) { + return resources.getString(R.string.yesterday) + } else { + "dd MMMM" + } + } + + else -> return resources.getString(R.string.today) + } + + return SimpleDateFormat(pattern, Locale.getDefault()).format(date) + } + + fun getLocalizedTime(resources: Resources, date: Long): String { + val now = Calendar.getInstance() + val then = Calendar.getInstance().also { it.timeInMillis = date } + + return when { + now.year != then.year -> { + "${now.year - then.year}${resources.getString(R.string.year_short).lowercase()}" + } + + now.month != then.month -> { + "${now.month - then.month}${resources.getString(R.string.month_short).lowercase()}" + } + + now.dayOfMonth != then.dayOfMonth -> { + val change = now.dayOfMonth - then.dayOfMonth + + if (change % 7 == 0) { + "${change / 7}${resources.getString(R.string.week_short).lowercase()}" + } else { + "$change${resources.getString(R.string.day_short).lowercase()}" + } + } + + now.hour == then.hour && now.minute == then.minute -> { + resources.getString(R.string.time_now).lowercase() + } + + else -> { + SimpleDateFormat("HH:mm", Locale.getDefault()).format(date) + } + } + } +} diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/util/VkUtils.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/util/VkUtils.kt new file mode 100644 index 00000000..7389532f --- /dev/null +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/util/VkUtils.kt @@ -0,0 +1,721 @@ +package com.meloda.app.fast.common.util + +//import android.content.Context +//import androidx.compose.ui.graphics.Color +//import androidx.compose.ui.text.AnnotatedString +//import androidx.compose.ui.text.SpanStyle +//import androidx.compose.ui.text.buildAnnotatedString +//import androidx.compose.ui.text.font.FontWeight +//import androidx.compose.ui.text.withStyle +//import com.meloda.app.fast.common.UiImage +//import com.meloda.app.fast.common.UiText +//import com.meloda.app.fast.common.extensions.orDots +//import com.meloda.app.fast.common.parseString +// +// +//@Suppress("MemberVisibilityCanBePrivate") +//object VkUtils { +// +// fun prepareMessageText(text: String, forConversations: Boolean = false): String { +// return text.apply { +// if (forConversations) { +// replace("\n", " ") +// } +// +// replace("&", "&") +// replace(""", "\"") +// replace("
", "\n") +// replace(">", ">") +// replace("<", "<") +// replace("
", "\n") +// replace("–", "-") +// trim() +// } +// } +// +// fun parseAttachments(baseAttachments: List?): List? { +// if (baseAttachments.isNullOrEmpty()) return null +// +// val attachments = mutableListOf() +// +// for (baseAttachment in baseAttachments) { +// when (baseAttachment.getPreparedType()) { +// AttachmentType.UNKNOWN -> continue +// +// AttachmentType.PHOTO -> { +// val photo = baseAttachment.photo ?: continue +// attachments += photo.toDomain() +// } +// +// AttachmentType.VIDEO -> { +// val video = baseAttachment.video ?: continue +// attachments += video.toDomain() +// } +// +// AttachmentType.AUDIO -> { +// val audio = baseAttachment.audio ?: continue +// attachments += audio.toDomain() +// } +// +// AttachmentType.FILE -> { +// val file = baseAttachment.file ?: continue +// attachments += file.toDomain() +// } +// +// AttachmentType.LINK -> { +// val link = baseAttachment.link ?: continue +// attachments += link.toDomain() +// } +// +// AttachmentType.MINI_APP -> { +// val miniApp = baseAttachment.miniApp ?: continue +// attachments += miniApp.toDomain() +// } +// +// AttachmentType.AUDIO_MESSAGE -> { +// val voiceMessage = baseAttachment.voiceMessage ?: continue +// attachments += voiceMessage.toDomain() +// } +// +// AttachmentType.STICKER -> { +// val sticker = baseAttachment.sticker ?: continue +// attachments += sticker.toDomain() +// } +// +// AttachmentType.GIFT -> { +// val gift = baseAttachment.gift ?: continue +// attachments += gift.toDomain() +// } +// +// AttachmentType.WALL -> { +// val wall = baseAttachment.wall ?: continue +// attachments += wall.toDomain() +// } +// +// AttachmentType.GRAFFITI -> { +// val graffiti = baseAttachment.graffiti ?: continue +// attachments += graffiti.toDomain() +// } +// +// AttachmentType.POLL -> { +// val poll = baseAttachment.poll ?: continue +// attachments += poll.toDomain() +// } +// +// AttachmentType.WALL_REPLY -> { +// val wallReply = baseAttachment.wallReply ?: continue +// attachments += wallReply.toDomain() +// } +// +// AttachmentType.CALL -> { +// val call = baseAttachment.call ?: continue +// attachments += call.toDomain() +// } +// +// AttachmentType.GROUP_CALL_IN_PROGRESS -> { +// val groupCall = baseAttachment.groupCall ?: continue +// attachments += groupCall.toDomain() +// } +// +// AttachmentType.CURATOR -> { +// val curator = baseAttachment.curator ?: continue +// attachments += curator.toDomain() +// } +// +// AttachmentType.EVENT -> { +// val event = baseAttachment.event ?: continue +// attachments += event.toDomain() +// } +// +// AttachmentType.STORY -> { +// val story = baseAttachment.story ?: continue +// attachments += story.toDomain() +// } +// +// AttachmentType.WIDGET -> { +// val widget = baseAttachment.widget ?: continue +// attachments += widget.toDomain() +// } +// +// AttachmentType.ARTIST -> { +// val artist = baseAttachment.artist ?: continue +// attachments += artist.toDomain() +// +// val audios = baseAttachment.audios ?: continue +// audios.map(VkAudioData::toDomain).let(attachments::addAll) +// } +// +// AttachmentType.AUDIO_PLAYLIST -> { +// val audioPlaylist = baseAttachment.audioPlaylist ?: continue +// attachments += audioPlaylist.toDomain() +// } +// +// AttachmentType.PODCAST -> { +// val podcast = baseAttachment.podcast ?: continue +// attachments += podcast.toDomain() +// } +// } +// } +// +// return attachments +// } +// +// fun getActionMessageText( +// context: Context, +// message: VkMessage?, +// youPrefix: String, +// messageUser: VkUserDomain?, +// messageGroup: VkGroupDomain?, +// action: VkMessage.Action?, +// actionUser: VkUserDomain?, +// actionGroup: VkGroupDomain?, +// ): AnnotatedString? { +// return when { +// message == null -> null +// action == null -> null +// +// else -> buildAnnotatedString { +// when (action) { +// VkMessage.Action.CHAT_CREATE -> { +// val text = message.actionText ?: return null +// +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isGroup() -> messageGroup?.name +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// val string = UiText.ResourceParams( +// UiR.string.message_action_chat_created, +// listOf(prefix, text) +// ).parseString(context).orEmpty() +// +// append(string) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// +// val textStartIndex = string.indexOf(text) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = textStartIndex, +// end = textStartIndex + text.length +// ) +// } +// +// VkMessage.Action.CHAT_TITLE_UPDATE -> { +// val text = message.actionText ?: return null +// +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isGroup() -> messageGroup?.name +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// val string = UiText.ResourceParams( +// UiR.string.message_action_chat_renamed, +// listOf(prefix, text) +// ).parseString(context).orEmpty() +// +// append(string) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// +// val textStartIndex = string.indexOf(text) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = textStartIndex, +// end = textStartIndex + text.length +// ) +// } +// +// VkMessage.Action.CHAT_PHOTO_UPDATE -> { +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isGroup() -> messageGroup?.name +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_photo_update, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } +// +// VkMessage.Action.CHAT_PHOTO_REMOVE -> { +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isGroup() -> messageGroup?.name +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_photo_remove, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } +// +// VkMessage.Action.CHAT_KICK_USER -> { +// val memberId = message.actionMemberId ?: return null +// val isUser = memberId > 0 +// val isGroup = memberId < 0 +// +// if (isUser && actionUser == null) return null +// if (isGroup && actionGroup == null) return null +// +// if (memberId == message.fromId) { +// val prefix = +// if (memberId == UserConfig.userId) youPrefix +// else actionUser.toString() +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_user_left, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } else { +// val prefix = +// if (message.fromId == UserConfig.userId) youPrefix +// else messageUser?.toString() ?: messageGroup?.toString().orDots() +// +// val postfix = +// if (memberId == UserConfig.userId) youPrefix.lowercase() +// else actionUser.toString() +// +// val string = UiText.ResourceParams( +// UiR.string.message_action_chat_user_kicked, +// listOf(prefix, postfix) +// ).parseString(context).orEmpty() +// +// append(string) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// +// val postfixStartIndex = string.indexOf(postfix) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = postfixStartIndex, +// end = postfixStartIndex + postfix.length +// ) +// } +// } +// +// VkMessage.Action.CHAT_INVITE_USER -> { +// val memberId = message.actionMemberId ?: 0 +// val isUser = memberId > 0 +// val isGroup = memberId < 0 +// +// if (isUser && actionUser == null) return null +// if (isGroup && actionGroup == null) return null +// +// if (memberId == message.fromId) { +// val prefix = +// if (memberId == UserConfig.userId) youPrefix +// else actionUser.toString() +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_user_returned, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } else { +// val prefix = +// if (message.fromId == UserConfig.userId) youPrefix +// else messageUser?.toString() ?: messageGroup?.toString().orDots() +// +// val postfix = +// if (memberId == UserConfig.userId) youPrefix.lowercase() +// else actionUser.toString() +// +// val string = UiText.ResourceParams( +// UiR.string.message_action_chat_user_invited, +// listOf(prefix, postfix) +// ).parseString(context).orEmpty() +// +// append(string) +// +// val postfixStartIndex = string.indexOf(postfix) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = postfixStartIndex, +// end = postfixStartIndex + postfix.length +// ) +// } +// } +// +// VkMessage.Action.CHAT_INVITE_USER_BY_LINK -> { +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_user_joined_by_link, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } +// +// VkMessage.Action.CHAT_INVITE_USER_BY_CALL -> { +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_user_joined_by_call, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } +// +// VkMessage.Action.CHAT_INVITE_USER_BY_CALL_LINK -> { +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_user_joined_by_call_link, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } +// +// VkMessage.Action.CHAT_PIN_MESSAGE -> { +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isGroup() -> messageGroup?.name +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_pin_message, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } +// +// VkMessage.Action.CHAT_UNPIN_MESSAGE -> { +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isGroup() -> messageGroup?.name +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_unpin_message, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } +// +// VkMessage.Action.CHAT_SCREENSHOT -> { +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isGroup() -> messageGroup?.name +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_screenshot, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } +// +// VkMessage.Action.CHAT_STYLE_UPDATE -> { +// val prefix = when { +// message.fromId == UserConfig.userId -> youPrefix +// message.isUser() -> messageUser?.toString() +// else -> return null +// } ?: return null +// +// UiText.ResourceParams( +// UiR.string.message_action_chat_style_update, +// listOf(prefix) +// ).parseString(context).orEmpty().let(::append) +// +// addStyle( +// style = SpanStyle(fontWeight = FontWeight.SemiBold), +// start = 0, +// end = prefix.length +// ) +// } +// } +// } +// } +// } +// +// fun getForwardsText(context: Context, message: VkMessage?): AnnotatedString? { +// return when { +// message == null -> null +// +// message.hasForwards() -> buildAnnotatedString { +// val forwards = message.forwards.orEmpty() +// +// withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) { +// append( +// UiText.Resource( +// if (forwards.size == 1) UiR.string.forwarded_message +// else UiR.string.forwarded_messages +// ).parseString(context) +// ) +// } +// } +// +// else -> null +// } +// } +// +// fun getAttachmentText( +// getText: (UiText) -> String, +// message: VkMessage? +// ): AnnotatedString? { +// return when { +// message == null -> null +// +// message.geoType != null -> buildAnnotatedString { +// withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) { +// when (message.geoType) { +// "point" -> getText(UiText.Resource(UiR.string.message_geo_point)) +// .let(::append) +// +// else -> getText(UiText.Resource(UiR.string.message_geo)) +// .let(::append) +// } +// } +// } +// +// message.hasAttachments() -> buildAnnotatedString { +// val attachments = message.attachments.orEmpty() +// +// withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) { +// if (attachments.size == 1) { +// getText(getAttachmentUiText(attachments.first())).let(::append) +// } else { +// when { +// isAttachmentsHaveOneType(attachments) -> { +// getText(getAttachmentUiText(attachments.first(), attachments.size)) +// .let(::append) +// } +// +// attachments.any { it.type == AttachmentType.ARTIST } -> { +// getText( +// getAttachmentUiText(attachments.first { it.type == AttachmentType.ARTIST }) +// ).let(::append) +// } +// +// else -> { +// getText(UiText.Resource(UiR.string.message_attachments_many)) +// .let(::append) +// } +// } +// } +// } +// } +// +// else -> null +// } +// } +// +// fun getAttachmentConversationIcon(message: VkMessage?): UiImage? { +// return message?.attachments?.let { attachments -> +// if (attachments.isEmpty()) return null +// if (attachments.size == 1 || isAttachmentsHaveOneType(attachments)) { +// message.geoType?.let { +// return UiImage.Resource(UiR.drawable.ic_map_marker) +// } +// +// getAttachmentIconByType(attachments.first().type) +// } else { +// UiImage.Resource(UiR.drawable.ic_baseline_attach_file_24) +// } +// } +// } +// +// +// +// fun getAttachmentUiText( +// attachment: VkAttachment, +// size: Int = 1, +// ): UiText { +// if (attachment.type.isMultiple()) { +// return when (attachment.type) { +// AttachmentType.PHOTO -> UiR.plurals.attachment_photos +// AttachmentType.VIDEO -> UiR.plurals.attachment_videos +// AttachmentType.AUDIO -> UiR.plurals.attachment_audios +// AttachmentType.FILE -> UiR.plurals.attachment_files +// else -> throw IllegalArgumentException("Unknown multiple type: ${attachment.type}") +// }.let { resId -> UiText.QuantityResource(resId, size) } +// } +// +// return when (attachment.type) { +// AttachmentType.UNKNOWN, +// AttachmentType.PHOTO, +// AttachmentType.VIDEO, +// AttachmentType.AUDIO, +// AttachmentType.FILE -> { +// throw IllegalArgumentException("Unknown multiple type: ${attachment.type}") +// } +// +// AttachmentType.LINK -> UiR.string.message_attachments_link +// AttachmentType.AUDIO_MESSAGE -> UiR.string.message_attachments_audio_message +// AttachmentType.MINI_APP -> UiR.string.message_attachments_mini_app +// AttachmentType.STICKER -> UiR.string.message_attachments_sticker +// AttachmentType.GIFT -> UiR.string.message_attachments_gift +// AttachmentType.WALL -> UiR.string.message_attachments_wall +// AttachmentType.GRAFFITI -> UiR.string.message_attachments_graffiti +// AttachmentType.POLL -> UiR.string.message_attachments_poll +// AttachmentType.WALL_REPLY -> UiR.string.message_attachments_wall_reply +// AttachmentType.CALL -> UiR.string.message_attachments_call +// AttachmentType.GROUP_CALL_IN_PROGRESS -> UiR.string.message_attachments_call_in_progress +// AttachmentType.CURATOR -> UiR.string.message_attachments_curator +// AttachmentType.EVENT -> UiR.string.message_attachments_event +// AttachmentType.STORY -> UiR.string.message_attachments_story +// AttachmentType.WIDGET -> UiR.string.message_attachments_widget +// AttachmentType.ARTIST -> UiR.string.message_attachments_artist +// AttachmentType.AUDIO_PLAYLIST -> UiR.string.message_attachments_audio_playlist +// AttachmentType.PODCAST -> UiR.string.message_attachments_podcast +// }.let(UiText::Resource) +// } +// +// fun getTextWithVisualizedMentions( +// originalText: String, +// mentionColor: Color, +// ): AnnotatedString = buildAnnotatedString { +// val regex = """\[(id|club)(\d+)\|([^]]+)]""".toRegex() +// +// val mentions = mutableListOf() +// +// var currentIndex = 0 +// val replacements = mutableListOf>() +// +// // TODO: 25/04/2024, Danil Nikolaev: check why not working ([id279494346|@iworld2rist] да убери ты Елену Шлипс от меня) +// val result = regex.replace(originalText) { matchResult -> +// val idPrefix = matchResult.groups[1]?.value.orEmpty() +// val startIndex = matchResult.range.first +// val endIndex = matchResult.range.last +// +// val id = matchResult.groups[2]?.value ?: "" +// val text = matchResult.groups[3]?.value ?: "" +// +// val replaced = +// text.substring(startIndex, endIndex + 1) +// .replace("[$idPrefix$id|$text]", text) +// +// val indexRange = +// (startIndex + currentIndex)..startIndex + currentIndex + replaced.length +// +// replacements.add(indexRange to replaced) +// +// mentions += MentionIndex( +// id = id.toIntOrNull() ?: -1, +// idPrefix = idPrefix, +// indexRange = indexRange +// ) +// +// currentIndex += replaced.length - (endIndex - startIndex + 1) +// +// replaced +// } +// +// append(result) +// +// mentions.forEach { mention -> +// val startIndex = mention.indexRange.first +// val endIndex = mention.indexRange.last +// +// addStyle( +// style = SpanStyle(color = mentionColor), +// start = startIndex, +// end = endIndex +// ) +// addStringAnnotation( +// tag = mention.idPrefix, +// annotation = mention.id.toString(), +// start = startIndex, +// end = endIndex +// ) +// } +// } +// +// +//} diff --git a/core/common/src/main/res/values/strings.xml b/core/common/src/main/res/values/strings.xml new file mode 100644 index 00000000..8b98b604 --- /dev/null +++ b/core/common/src/main/res/values/strings.xml @@ -0,0 +1,10 @@ + + + Yesterday + Today + Y + M + W + D + Now + diff --git a/core/data/.gitignore b/core/data/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/data/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts new file mode 100644 index 00000000..73ebf969 --- /dev/null +++ b/core/data/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.org.jetbrains.kotlin.android) +} + +group = "com.meloda.app.fast.data" + +android { + namespace = "com.meloda.app.fast.data" + compileSdk = Configs.compileSdk + + defaultConfig { + minSdk = Configs.minSdk + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = Configs.java + targetCompatibility = Configs.java + } + kotlinOptions { + jvmTarget = Configs.java.toString() + } +} + +dependencies { + api(projects.core.common) + api(projects.core.datastore) + api(projects.core.model) + api(projects.core.network) + api(projects.core.database) + + implementation(libs.koin.android) + + // TODO: 05/05/2024, Danil Nikolaev: research, maybe remove + implementation(libs.retrofit) + implementation(libs.eithernet) +} diff --git a/core/data/src/main/AndroidManifest.xml b/core/data/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/core/data/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUpdatesParser.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUpdatesParser.kt new file mode 100644 index 00000000..59c7638f --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUpdatesParser.kt @@ -0,0 +1,354 @@ +package com.meloda.app.fast.data + +import android.util.Log +import com.meloda.app.fast.common.UserConfig +import com.meloda.app.fast.common.VkConstants +import com.meloda.app.fast.common.extensions.asInt +import com.meloda.app.fast.common.extensions.listenValue +import com.meloda.app.fast.common.extensions.toList +import com.meloda.app.fast.data.api.messages.MessagesUseCase +import com.meloda.app.fast.model.ApiEvent +import com.meloda.app.fast.model.InteractionType +import com.meloda.app.fast.model.LongPollEvent +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class LongPollUpdatesParser( + private val messagesUseCase: MessagesUseCase +) { + private val job = SupervisorJob() + + private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + Log.d("LongPollUpdatesParser", "error: $throwable") + throwable.printStackTrace() + } + + private val coroutineContext: CoroutineContext + get() = Dispatchers.Default + job + exceptionHandler + + private val coroutineScope = CoroutineScope(coroutineContext) + + private val listenersMap: MutableMap>> = + mutableMapOf() + + fun parseNextUpdate(event: List) { + val eventId = event.first().asInt() + + val eventType: ApiEvent = try { + ApiEvent.parse(eventId) + } catch (e: Exception) { + e.printStackTrace() + Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event") + return + } + + when (eventType) { + ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event) + ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event) + ApiEvent.MESSAGE_NEW -> parseMessageNew(eventType, event) + ApiEvent.MESSAGE_EDIT -> parseMessageEdit(eventType, event) + ApiEvent.MESSAGE_READ_INCOMING -> parseMessageReadIncoming(eventType, event) + ApiEvent.MESSAGE_READ_OUTGOING -> parseMessageReadOutgoing(eventType, event) + ApiEvent.MESSAGES_DELETED -> parseMessagesDeleted(eventType, event) + ApiEvent.PIN_UNPIN_CONVERSATION -> parseConversationPinStateChanged(eventType, event) + + ApiEvent.TYPING, + ApiEvent.AUDIO_MESSAGE_RECORDING, + ApiEvent.PHOTO_UPLOADING, + ApiEvent.VIDEO_UPLOADING, + ApiEvent.FILE_UPLOADING -> parseInteraction(eventType, event) + + ApiEvent.UNREAD_COUNT_UPDATE -> onNewEvent(eventType, event) + } + } + + private fun onNewEvent(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "newEvent: $eventType: $event") + } + + private fun parseInteraction(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + + val interactionType = when (eventType) { + ApiEvent.TYPING -> InteractionType.Typing + ApiEvent.AUDIO_MESSAGE_RECORDING -> InteractionType.VoiceMessage + ApiEvent.PHOTO_UPLOADING -> InteractionType.Photo + ApiEvent.VIDEO_UPLOADING -> InteractionType.Video + ApiEvent.FILE_UPLOADING -> InteractionType.File + else -> return + } + + val peerId = event[1].asInt() + val userIds = event[2].toList(Any::asInt).filter { it != UserConfig.userId } + val totalCount = event[3].asInt() + val timestamp = event[4].asInt() + + // if userIds contains only account's id, then we don't need to show our status + if (userIds.isEmpty()) return + + coroutineScope.launch { + listenersMap[eventType]?.let { listeners -> + listeners.forEach { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollEvent.Interaction( + interactionType = interactionType, + peerId = peerId, + userIds = userIds, + totalCount = totalCount, + timestamp = timestamp + ) + ) + } + } + } + } + + private fun parseConversationPinStateChanged(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + + val peerId = event[1].asInt() + val majorId = event[2].asInt() + + coroutineScope.launch { + listenersMap[ApiEvent.PIN_UNPIN_CONVERSATION]?.let { listeners -> + listeners.forEach { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollEvent.VkConversationPinStateChangedEvent( + peerId = peerId, + majorId = majorId + ) + ) + } + } + } + } + + private fun parseMessageSetFlags(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + } + + private fun parseMessageClearFlags(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + } + + private fun parseMessageNew(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + val messageId = event[1].asInt() + + coroutineScope.launch(Dispatchers.IO) { + val newMessageEvent: LongPollEvent.VkMessageNewEvent? = + loadNormalMessage( + eventType, + messageId + ) + + newMessageEvent?.let { event -> + listenersMap[ApiEvent.MESSAGE_NEW]?.let { + it.map { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent(event) + } + } + } + } + } + + private fun parseMessageEdit(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + val messageId = event[1].asInt() + + coroutineScope.launch { + val editedMessageEvent: LongPollEvent.VkMessageEditEvent? = + loadNormalMessage( + eventType, + messageId + ) + + editedMessageEvent?.let { event -> + listenersMap[ApiEvent.MESSAGE_EDIT]?.let { + it.map { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent(event) + } + } + } + } + } + + private fun parseMessageReadIncoming(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + val peerId = event[1].asInt() + val messageId = event[2].asInt() + val unreadCount = event[3].asInt() + + coroutineScope.launch { + listenersMap[ApiEvent.MESSAGE_READ_INCOMING]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollEvent.VkMessageReadIncomingEvent( + peerId = peerId, + messageId = messageId, + unreadCount = unreadCount + ) + ) + } + } + } + } + + private fun parseMessageReadOutgoing(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + val peerId = event[1].asInt() + val messageId = event[2].asInt() + val unreadCount = event[3].asInt() + + coroutineScope.launch { + listenersMap[ApiEvent.MESSAGE_READ_OUTGOING]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollEvent.VkMessageReadOutgoingEvent( + peerId = peerId, + messageId = messageId, + unreadCount = unreadCount + ) + ) + } + } + } + } + + private fun parseMessagesDeleted(eventType: ApiEvent, event: List) { + Log.d("LongPollUpdatesParser", "$eventType: $event") + } + + private suspend fun loadNormalMessage( + eventType: ApiEvent, + messageId: Int + ): T? = suspendCoroutine { + coroutineScope.launch(Dispatchers.IO) { + messagesUseCase.getById( + messageId = messageId, + extended = true, + fields = VkConstants.ALL_FIELDS + ).listenValue(this) { state -> + state.processState( + error = { error -> + Log.e("LongPollUpdatesParser", "loadNormalMessage: error: $error") + }, + success = { response -> + response?.let { message -> + VkMemoryCache[message.id] = message + messagesUseCase.storeMessage(message) + + val resumeValue: LongPollEvent? = when (eventType) { + ApiEvent.MESSAGE_NEW -> LongPollEvent.VkMessageNewEvent(message) + ApiEvent.MESSAGE_EDIT -> LongPollEvent.VkMessageEditEvent(message) + + else -> null + } + + resumeValue?.let { value -> it.resume(value as T) } + } ?: it.resume(null) + } + ) + } + } + } + + private fun registerListener( + eventType: ApiEvent, + listener: VkEventCallback + ) { + listenersMap.let { map -> + map[eventType] = (map[eventType] ?: mutableListOf()).also { it.add(listener) } + } + } + + private fun registerListeners( + eventTypes: List, + listener: VkEventCallback + ) { + eventTypes.forEach { eventType -> registerListener(eventType, listener) } + } + + fun onConversationPinStateChanged(listener: VkEventCallback) { + registerListener(ApiEvent.PIN_UNPIN_CONVERSATION, listener) + } + + fun onConversationPinStateChanged(block: (LongPollEvent.VkConversationPinStateChangedEvent) -> Unit) { + onConversationPinStateChanged(assembleEventCallback(block)) + } + + fun onMessageIncomingRead(listener: VkEventCallback) { + registerListener(ApiEvent.MESSAGE_READ_INCOMING, listener) + } + + fun onMessageIncomingRead(block: (LongPollEvent.VkMessageReadIncomingEvent) -> Unit) { + onMessageIncomingRead(assembleEventCallback(block)) + } + + fun onMessageOutgoingRead(listener: VkEventCallback) { + registerListener(ApiEvent.MESSAGE_READ_OUTGOING, listener) + } + + fun onMessageOutgoingRead(block: (LongPollEvent.VkMessageReadOutgoingEvent) -> Unit) { + onMessageOutgoingRead(assembleEventCallback(block)) + } + + fun onNewMessage(listener: VkEventCallback) { + registerListener(ApiEvent.MESSAGE_NEW, listener) + } + + fun onNewMessage(block: (LongPollEvent.VkMessageNewEvent) -> Unit) { + onNewMessage(assembleEventCallback(block)) + } + + fun onMessageEdited(listener: VkEventCallback) { + registerListener(ApiEvent.MESSAGE_EDIT, listener) + } + + fun onMessageEdited(block: (LongPollEvent.VkMessageEditEvent) -> Unit) { + onMessageEdited(assembleEventCallback(block)) + } + + fun onInteractions(listener: VkEventCallback) { + registerListeners( + eventTypes = listOf( + ApiEvent.TYPING, + ApiEvent.AUDIO_MESSAGE_RECORDING, + ApiEvent.PHOTO_UPLOADING, + ApiEvent.VIDEO_UPLOADING, + ApiEvent.FILE_UPLOADING + ), + listener = listener + ) + } + + fun onInteractions(block: (LongPollEvent.Interaction) -> Unit) { + onInteractions(assembleEventCallback(block)) + } + + fun clearListeners() { + listenersMap.clear() + } +} + +internal inline fun assembleEventCallback( + crossinline block: (R) -> Unit, +): VkEventCallback { + return VkEventCallback { event -> block.invoke(event) } +} + +fun interface VkEventCallback { + fun onEvent(event: T) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUseCase.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUseCase.kt new file mode 100644 index 00000000..dd5acee8 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUseCase.kt @@ -0,0 +1,23 @@ +package com.meloda.app.fast.data + +import com.meloda.app.fast.model.api.data.LongPollUpdates +import com.meloda.app.fast.model.api.data.VkLongPollData +import kotlinx.coroutines.flow.Flow + +interface LongPollUseCase { + + fun getLongPollServer( + needPts: Boolean, + version: Int + ): Flow> + + fun getLongPollUpdates( + serverUrl: String, + act: String = "a_check", + key: String, + ts: Int, + wait: Int, + mode: Int, + version: Int + ): Flow> +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUseCaseImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUseCaseImpl.kt new file mode 100644 index 00000000..568d36c6 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/LongPollUseCaseImpl.kt @@ -0,0 +1,49 @@ +package com.meloda.app.fast.data + +import com.meloda.app.fast.data.api.longpoll.LongPollRepository +import com.meloda.app.fast.model.api.data.LongPollUpdates +import com.meloda.app.fast.model.api.data.VkLongPollData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class LongPollUseCaseImpl( + private val repository: LongPollRepository +) : LongPollUseCase { + + override fun getLongPollServer( + needPts: Boolean, + version: Int + ): Flow> = flow { + emit(State.Loading) + + val newState = repository.getLongPollServer( + needPts = needPts, + version = version + ).mapToState() + + emit(newState) + } + + override fun getLongPollUpdates( + serverUrl: String, + act: String, + key: String, + ts: Int, + wait: Int, + mode: Int, + version: Int + ): Flow> = flow { + emit(State.Loading) + + val newState = repository.getLongPollUpdates( + serverUrl, + act = act, + key = key, + ts = ts, + wait = wait, + mode = mode, + version = version + ).mapToState() + emit(newState) + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/State.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/State.kt new file mode 100644 index 00000000..bf3b663d --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/State.kt @@ -0,0 +1,76 @@ +package com.meloda.app.fast.data + +import com.meloda.app.fast.network.OAuthErrorDomain +import com.meloda.app.fast.network.RestApiErrorDomain +import com.slack.eithernet.ApiResult +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.map + +sealed class State { + + data object Idle : State() + data class Success(val data: T) : State() + data object Loading : State() + + sealed class Error : State() { + + data class ApiError( + val errorCode: Int, + val errorMessage: String, + ) : Error() + + data object ConnectionError : Error() + + data object Unknown : Error() + + data object InternalError : Error() + + data class OAuthError(val error: OAuthErrorDomain) : Error() + } + + fun isLoading(): Boolean = this is Loading + + companion object { + + val UNKNOWN_ERROR = Error.Unknown + } +} + +inline fun State.processState( + error: (error: State.Error) -> (Unit), + success: (data: T) -> (Unit), + idle: (() -> (Unit)) = {}, + loading: (() -> (Unit)) = {}, +) { + when (this) { + is State.Error -> error(this) + State.Idle -> idle() + State.Loading -> loading() + is State.Success -> success(data) + } +} + +inline fun Flow>.mapSuccess( + crossinline transform: suspend (value: T) -> R +): Flow = filterIsInstance>() + .map { state -> transform.invoke(state.data) } + +fun RestApiErrorDomain?.toStateApiError(): State.Error = when (this) { + null -> State.Error.ConnectionError + else -> State.Error.ApiError(code, message) +} + +fun OAuthErrorDomain?.toStateApiError(): State.Error = when (this) { + null -> State.Error.ConnectionError + else -> State.Error.OAuthError(this) +} + +fun ApiResult.mapToState() = when (this) { + is ApiResult.Success -> State.Success(this.value) + + is ApiResult.Failure.NetworkFailure -> State.Error.ConnectionError + is ApiResult.Failure.UnknownFailure -> State.UNKNOWN_ERROR + is ApiResult.Failure.HttpFailure -> this.error.toStateApiError() + is ApiResult.Failure.ApiFailure -> this.error.toStateApiError() +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/VkGroupsMap.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/VkGroupsMap.kt new file mode 100644 index 00000000..3c92b118 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/VkGroupsMap.kt @@ -0,0 +1,47 @@ +package com.meloda.app.fast.data + +import com.meloda.app.fast.model.api.data.VkMessageData +import com.meloda.app.fast.model.api.domain.VkConversation +import com.meloda.app.fast.model.api.domain.VkGroupDomain +import com.meloda.app.fast.model.api.domain.VkMessage +import kotlin.math.abs + +class VkGroupsMap( + private val groups: List +) { + + private val map: HashMap by lazy { + HashMap(groups.associateBy(VkGroupDomain::id)) + } + + fun groups(): List = map.values.toList() + + fun conversationGroup(conversation: VkConversation): VkGroupDomain? = + if (!conversation.peerType.isGroup()) null + else map[abs(conversation.id)] + + fun messageActionGroup(message: VkMessage): VkGroupDomain? = + if (message.actionMemberId == null || message.actionMemberId!! >= 0) null + else map[abs(message.actionMemberId!!)] + + fun messageActionGroup(message: VkMessageData): VkGroupDomain? = + if (message.action?.memberId == null || message.action!!.memberId!! >= 0) null + else map[abs(message.action!!.memberId!!)] + + fun messageGroup(message: VkMessage): VkGroupDomain? = + if (!message.isGroup()) null + else map[abs(message.fromId)] + + fun messageGroup(message: VkMessageData): VkGroupDomain? = + if (message.fromId >= 0) null + else map[abs(message.fromId)] + + fun group(groupId: Int): VkGroupDomain? = map[abs(groupId)] + + companion object { + + fun forGroups(groups: List): VkGroupsMap = VkGroupsMap(groups = groups) + + fun List.toGroupsMap(): VkGroupsMap = forGroups(this) + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/VkMemoryCache.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/VkMemoryCache.kt new file mode 100644 index 00000000..4f3bf277 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/VkMemoryCache.kt @@ -0,0 +1,119 @@ +package com.meloda.app.fast.data + +import com.meloda.app.fast.model.api.domain.VkContactDomain +import com.meloda.app.fast.model.api.domain.VkConversation +import com.meloda.app.fast.model.api.domain.VkGroupDomain +import com.meloda.app.fast.model.api.domain.VkMessage +import com.meloda.app.fast.model.api.domain.VkUser +import kotlin.math.abs + +object VkMemoryCache { + + private val users: HashMap = hashMapOf() + private val groups: HashMap = hashMapOf() + private val messages: HashMap = hashMapOf() + private val conversations: HashMap = hashMapOf() + private val contacts: HashMap = hashMapOf() + + fun appendUsers(users: List) { + users.forEach { user -> VkMemoryCache.users[user.id] = user } + } + + fun appendGroups(groups: List) { + groups.forEach { group -> VkMemoryCache.groups[abs(group.id)] = group } + } + + fun appendMessages(messages: List) { + messages.forEach { message -> VkMemoryCache.messages[message.id] = message } + } + + fun appendConversations(conversations: List) { + conversations.forEach { conversation -> + VkMemoryCache.conversations[conversation.id] = conversation + } + } + + fun appendContacts(contacts: List) { + contacts.forEach { contact -> VkMemoryCache.contacts[contact.userId] = contact } + } + + operator fun set(userId: Int, user: VkUser) { + users[userId] = user + } + + operator fun set(groupId: Int, group: VkGroupDomain) { + groups[groupId] = group + } + + operator fun set(messageId: Int, message: VkMessage) { + messages[messageId] = message + } + + operator fun set(conversationId: Int, conversation: VkConversation) { + conversations[conversationId] = conversation + } + + operator fun set(contactId: Int, contact: VkContactDomain) { + contacts[contactId] = contact + } + + fun getUser(id: Int): VkUser? { + return getUsers(id).firstOrNull() + } + + fun getUsers(vararg ids: Int): List { + return getUsers(ids.toList()) + } + + fun getUsers(ids: List): List { + return ids.mapNotNull { id -> users[id] } + } + + fun getGroup(id: Int): VkGroupDomain? { + return getGroups(id).firstOrNull() + } + + fun getGroups(vararg ids: Int): List { + return getGroups(ids.toList()) + } + + fun getGroups(ids: List): List { + return ids.mapNotNull { id -> groups[id] } + } + + fun getMessage(id: Int): VkMessage? { + return getMessages(id).firstOrNull() + } + + fun getMessages(vararg ids: Int): List { + return getMessages(ids.toList()) + } + + fun getMessages(ids: List): List { + return ids.mapNotNull { id -> messages[id] } + } + + fun getConversation(id: Int): VkConversation? { + return getConversations(id).firstOrNull() + } + + fun getConversations(vararg ids: Int): List { + return getConversations(ids.toList()) + } + + fun getConversations(ids: List): List { + return ids.mapNotNull { id -> conversations[id] } + } + + fun getContact(id: Int): VkContactDomain? { + return getContacts(id).firstOrNull() + } + + fun getContacts(vararg ids: Int): List { + return getContacts(ids.toList()) + } + + fun getContacts(ids: List): List { + return ids.mapNotNull { id -> contacts[id] } + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/VkUsersMap.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/VkUsersMap.kt new file mode 100644 index 00000000..60c14f91 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/VkUsersMap.kt @@ -0,0 +1,46 @@ +package com.meloda.app.fast.data + +import com.meloda.app.fast.model.api.data.VkMessageData +import com.meloda.app.fast.model.api.domain.VkConversation +import com.meloda.app.fast.model.api.domain.VkMessage +import com.meloda.app.fast.model.api.domain.VkUser + +class VkUsersMap( + private val users: List +) { + + private val map: HashMap by lazy { + HashMap(users.associateBy(VkUser::id)) + } + + fun users(): List = map.values.toList() + + fun conversationUser(conversation: VkConversation): VkUser? = + if (!conversation.peerType.isUser()) null + else map[conversation.id] + + fun messageActionUser(message: VkMessage): VkUser? = + if (message.actionMemberId == null || message.actionMemberId!! <= 0) null + else map[message.actionMemberId] + + fun messageActionUser(message: VkMessageData): VkUser? = + if (message.action?.memberId == null || message.action!!.memberId!! <= 0) null + else map[message.action!!.memberId] + + fun messageUser(message: VkMessage): VkUser? = + if (!message.isUser()) null + else map[message.fromId] + + fun messageUser(message: VkMessageData): VkUser? = + if (message.fromId > 0) map[message.fromId] + else null + + fun user(userId: Int): VkUser? = map[userId] + + companion object { + + fun forUsers(users: List): VkUsersMap = VkUsersMap(users = users) + + fun List.toUsersMap(): VkUsersMap = forUsers(this) + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountRepository.kt new file mode 100644 index 00000000..4c0d052c --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountRepository.kt @@ -0,0 +1,15 @@ +package com.meloda.app.fast.data.api.account + +import com.meloda.app.fast.model.api.requests.AccountSetOfflineRequest +import com.meloda.app.fast.model.api.requests.AccountSetOnlineRequest + +interface AccountRepository { + + suspend fun setOnline( + params: AccountSetOnlineRequest + ): Boolean + + suspend fun setOffline( + params: AccountSetOfflineRequest + ): Boolean +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountRepositoryImpl.kt new file mode 100644 index 00000000..2d6a53cd --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountRepositoryImpl.kt @@ -0,0 +1,19 @@ +package com.meloda.app.fast.data.api.account + +import com.meloda.app.fast.model.api.requests.AccountSetOfflineRequest +import com.meloda.app.fast.model.api.requests.AccountSetOnlineRequest +import com.meloda.app.fast.network.service.account.AccountService + +// TODO: 05/05/2024, Danil Nikolaev: implement +class AccountRepositoryImpl( + private val accountService: AccountService +) : com.meloda.app.fast.data.api.account.AccountRepository { + + override suspend fun setOnline(params: AccountSetOnlineRequest): Boolean { + return false + } + + override suspend fun setOffline(params: AccountSetOfflineRequest): Boolean { + return false + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountUseCase.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountUseCase.kt new file mode 100644 index 00000000..c52c312f --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountUseCase.kt @@ -0,0 +1,16 @@ +package com.meloda.app.fast.data.api.account + +import com.meloda.app.fast.data.State +import kotlinx.coroutines.flow.Flow + +interface AccountUseCase { + + suspend fun setOnline( + voip: Boolean, + accessToken: String + ): Flow> + + suspend fun setOffline( + accessToken: String + ): Flow> +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountUseCaseImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountUseCaseImpl.kt new file mode 100644 index 00000000..7bdafefc --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/account/AccountUseCaseImpl.kt @@ -0,0 +1,49 @@ +package com.meloda.app.fast.data.api.account + +import com.meloda.app.fast.data.State +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +// TODO: 05/05/2024, Danil Nikolaev: implement +class AccountUseCaseImpl( + private val accountRepository: com.meloda.app.fast.data.api.account.AccountRepository +) : com.meloda.app.fast.data.api.account.AccountUseCase { + + override suspend fun setOnline( + voip: Boolean, + accessToken: String + ): Flow> = flow { +// emit(com.meloda.app.fast.data.State.Loading) +// +// val newState = accountRepository.setOnline( +// params = AccountSetOnlineRequest( +// voip = voip, +// accessToken = accessToken +// ) +// ).fold( +// onSuccess = { response -> com.meloda.app.fast.data.State.Success(response) }, +// onNetworkFailure = { com.meloda.app.fast.data.State.Error.ConnectionError }, +// onUnknownFailure = { com.meloda.app.fast.data.State.UNKNOWN_ERROR }, +// onHttpFailure = { result -> result.error.toStateApiError() }, +// onApiFailure = { result -> result.error.toStateApiError() } +// ) +// emit(newState) + } + + override suspend fun setOffline( + accessToken: String + ): Flow> = flow { + emit(com.meloda.app.fast.data.State.Loading) + +// val newState = accountRepository.setOffline( +// params = AccountSetOfflineRequest(accessToken = accessToken) +// ).fold( +// onSuccess = { response -> com.meloda.app.fast.data.State.Success(response) }, +// onNetworkFailure = { com.meloda.app.fast.data.State.Error.ConnectionError }, +// onUnknownFailure = { com.meloda.app.fast.data.State.UNKNOWN_ERROR }, +// onHttpFailure = { result -> result.error.toStateApiError() }, +// onApiFailure = { result -> result.error.toStateApiError() } +// ) +// emit(newState) + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/audios/AudiosRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/audios/AudiosRepository.kt new file mode 100644 index 00000000..33df923a --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/audios/AudiosRepository.kt @@ -0,0 +1,26 @@ +package com.meloda.app.fast.data.api.audios + +import com.meloda.app.fast.model.api.responses.AudiosGetUploadServerResponse +import com.meloda.app.fast.network.ApiResponse +import com.meloda.app.fast.network.RestApiError +import com.meloda.app.fast.network.service.audios.AudiosService +import com.slack.eithernet.ApiResult +import okhttp3.MultipartBody + +class AudiosRepository( + private val audiosService: AudiosService +) { + + suspend fun getUploadServer(): ApiResult, RestApiError> = + audiosService.getUploadServer() + + suspend fun upload(url: String, file: MultipartBody.Part) = audiosService.upload(url, file) + + suspend fun save(server: Int, audio: String, hash: String) = audiosService.save( + mapOf( + "server" to server.toString(), + "audio" to audio, + "hash" to hash + ) + ) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/auth/AuthRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/auth/AuthRepository.kt new file mode 100644 index 00000000..6ebfc4fa --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/auth/AuthRepository.kt @@ -0,0 +1,18 @@ +package com.meloda.app.fast.data.api.auth + +import com.meloda.app.fast.model.api.requests.AuthDirectRequest +import com.meloda.app.fast.model.api.responses.AuthDirectResponse +import com.meloda.app.fast.model.api.responses.SendSmsResponse +import com.meloda.app.fast.network.OAuthErrorDomain +import com.slack.eithernet.ApiResult + +interface AuthRepository { + +// suspend fun auth( +// params: AuthDirectRequest +// ): ApiResult + + suspend fun sendSms( + validationSid: String + ): SendSmsResponse +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/auth/AuthRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/auth/AuthRepositoryImpl.kt new file mode 100644 index 00000000..f6489814 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/auth/AuthRepositoryImpl.kt @@ -0,0 +1,35 @@ +package com.meloda.app.fast.data.api.auth + +import com.meloda.app.fast.model.api.requests.AuthDirectRequest +import com.meloda.app.fast.model.api.responses.AuthDirectResponse +import com.meloda.app.fast.model.api.responses.SendSmsResponse +import com.meloda.app.fast.network.OAuthErrorDomain +import com.meloda.app.fast.network.service.auth.AuthService +import com.slack.eithernet.ApiResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class AuthRepositoryImpl( + private val authService: AuthService +) : AuthRepository { + +// override suspend fun auth( +// params: AuthDirectRequest +// ): ApiResult { +// +// } + + // TODO: 05/05/2024, Danil Nikolaev: implement + override suspend fun sendSms( + validationSid: String + ): SendSmsResponse = withContext(Dispatchers.IO) { + SendSmsResponse( + validationSid = null, delay = null, validationType = null, validationResend = null + + ) +// authService.sendSms(validationSid).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsRepository.kt new file mode 100644 index 00000000..de2f64f6 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsRepository.kt @@ -0,0 +1,18 @@ +package com.meloda.app.fast.data.api.conversations + +import com.meloda.app.fast.model.api.domain.VkConversation +import com.meloda.app.fast.network.RestApiErrorDomain +import com.slack.eithernet.ApiResult + +interface ConversationsRepository { + + suspend fun getConversations( + count: Int?, + offset: Int? + ): ApiResult, RestApiErrorDomain> + + suspend fun storeConversations(conversations: List) + suspend fun delete(peerId: Int): ApiResult + suspend fun pin(peerId: Int): ApiResult + suspend fun unpin(peerId: Int): ApiResult +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsRepositoryImpl.kt new file mode 100644 index 00000000..2af0c929 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsRepositoryImpl.kt @@ -0,0 +1,109 @@ +package com.meloda.app.fast.data.api.conversations + +import com.meloda.app.fast.common.VkConstants +import com.meloda.app.fast.data.VkGroupsMap +import com.meloda.app.fast.data.VkMemoryCache +import com.meloda.app.fast.data.VkUsersMap +import com.meloda.app.fast.database.dao.ConversationDao +import com.meloda.app.fast.model.api.data.VkContactData +import com.meloda.app.fast.model.api.data.VkGroupData +import com.meloda.app.fast.model.api.data.VkUserData +import com.meloda.app.fast.model.api.data.asDomain +import com.meloda.app.fast.model.api.domain.VkConversation +import com.meloda.app.fast.model.api.domain.asEntity +import com.meloda.app.fast.model.api.requests.ConversationsDeleteRequest +import com.meloda.app.fast.model.api.requests.ConversationsGetRequest +import com.meloda.app.fast.model.api.requests.ConversationsPinRequest +import com.meloda.app.fast.model.api.requests.ConversationsUnpinRequest +import com.meloda.app.fast.network.RestApiErrorDomain +import com.meloda.app.fast.network.mapApiDefault +import com.meloda.app.fast.network.mapApiResult +import com.meloda.app.fast.network.service.conversations.ConversationsService +import com.slack.eithernet.ApiResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class ConversationsRepositoryImpl( + private val conversationsService: ConversationsService, + private val conversationDao: ConversationDao +) : ConversationsRepository { + + override suspend fun getConversations( + count: Int?, + offset: Int? + ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { + val requestModel = ConversationsGetRequest( + count = count, + offset = offset, + fields = VkConstants.ALL_FIELDS, + filter = "all", + extended = true, + startMessageId = null + ) + + conversationsService.getConversations(requestModel.map).mapApiResult( + successMapper = { apiResponse -> + val response = apiResponse.requireResponse() + + val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain) + val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain) + val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain) + + val usersMap = VkUsersMap.forUsers(profilesList) + val groupsMap = VkGroupsMap.forGroups(groupsList) + + VkMemoryCache.appendUsers(profilesList) + VkMemoryCache.appendGroups(groupsList) + VkMemoryCache.appendContacts(contactsList) + + response.items.map { item -> + val lastMessage = item.lastMessage?.asDomain()?.let { message -> + message.copy( + user = usersMap.messageUser(message), + group = groupsMap.messageGroup(message), + actionUser = usersMap.messageActionUser(message), + actionGroup = groupsMap.messageActionGroup(message) + ).also { VkMemoryCache[message.id] = it } + } + item.conversation.asDomain(lastMessage).let { conversation -> + conversation.copy( + user = usersMap.conversationUser(conversation), + group = groupsMap.conversationGroup(conversation) + ).also { VkMemoryCache[conversation.id] = it } + } + } + }, + errorMapper = { error -> + error?.toDomain() + } + ) + } + + override suspend fun storeConversations(conversations: List) { + conversationDao.insertAll(conversations.map(VkConversation::asEntity)) + } + + override suspend fun delete(peerId: Int): ApiResult = + withContext(Dispatchers.IO) { + val requestModel = ConversationsDeleteRequest(peerId = peerId) + + conversationsService.delete(requestModel.map).mapApiResult( + successMapper = { response -> response.requireResponse().lastDeletedId }, + errorMapper = { error -> error?.toDomain() } + ) + } + + override suspend fun pin( + peerId: Int + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = ConversationsPinRequest(peerId = peerId) + conversationsService.pin(requestModel.map).mapApiDefault() + } + + override suspend fun unpin( + peerId: Int + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = ConversationsUnpinRequest(peerId = peerId) + conversationsService.unpin(requestModel.map).mapApiDefault() + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsUseCase.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsUseCase.kt new file mode 100644 index 00000000..e6663ea5 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/conversations/ConversationsUseCase.kt @@ -0,0 +1,19 @@ +package com.meloda.app.fast.data.api.conversations + +import com.meloda.app.fast.data.State +import com.meloda.app.fast.model.api.domain.VkConversation +import kotlinx.coroutines.flow.Flow + +interface ConversationsUseCase { + + fun getConversations( + count: Int?, + offset: Int?, + ): Flow>> + + fun delete(peerId: Int): Flow> + + fun changePinState(peerId: Int, pin: Boolean): Flow> + + suspend fun storeConversations(conversations: List) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/files/FilesRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/files/FilesRepository.kt new file mode 100644 index 00000000..57930be2 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/files/FilesRepository.kt @@ -0,0 +1,31 @@ +package com.meloda.app.fast.data.api.files + +import com.meloda.app.fast.network.service.files.FilesService +import okhttp3.MultipartBody + +class FilesRepository( + private val filesService: FilesService +) { + + // TODO: 05/05/2024, Danil Nikolaev: reimplement +// enum class FileType(val value: String) { +// @Json(name = "doc") +// FILE("doc"), +// +// @Json(name = "audio_message") +// AUDIO_MESSAGE("audio_message") +// } +// +// suspend fun getMessagesUploadServer(peerId: Int, type: FileType) = +// filesService.getUploadServer( +// mapOf( +// "peer_id" to peerId.toString(), +// "type" to type.value +// ) +// ) + + suspend fun uploadFile(url: String, file: MultipartBody.Part) = filesService.upload(url, file) + + suspend fun saveMessageFile(file: String) = filesService.save(mapOf("file" to file)) + +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsRepository.kt new file mode 100644 index 00000000..498cbfab --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsRepository.kt @@ -0,0 +1,26 @@ +package com.meloda.app.fast.data.api.friends + +import com.meloda.app.fast.model.FriendsInfo +import com.meloda.app.fast.model.api.domain.VkUser +import com.meloda.app.fast.network.RestApiErrorDomain +import com.slack.eithernet.ApiResult + +interface FriendsRepository { + + suspend fun getAllFriends( + count: Int?, + offset: Int? + ): ApiResult + + suspend fun getFriends( + count: Int?, + offset: Int? + ): ApiResult, RestApiErrorDomain> + + suspend fun getOnlineFriends( + count: Int?, + offset: Int? + ): ApiResult, RestApiErrorDomain> + + suspend fun storeUsers(users: List) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsRepositoryImpl.kt new file mode 100644 index 00000000..0c43874b --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsRepositoryImpl.kt @@ -0,0 +1,83 @@ +package com.meloda.app.fast.data.api.friends + +import com.meloda.app.fast.common.VkConstants +import com.meloda.app.fast.data.VkMemoryCache +import com.meloda.app.fast.database.dao.UsersDao +import com.meloda.app.fast.model.FriendsInfo +import com.meloda.app.fast.model.api.data.VkUserData +import com.meloda.app.fast.model.api.domain.VkUser +import com.meloda.app.fast.model.api.domain.asEntity +import com.meloda.app.fast.model.api.requests.GetFriendsRequest +import com.meloda.app.fast.model.api.requests.GetOnlineFriendsRequest +import com.meloda.app.fast.network.RestApiErrorDomain +import com.meloda.app.fast.network.mapApiDefault +import com.meloda.app.fast.network.mapApiResult +import com.meloda.app.fast.network.service.friends.FriendsService +import com.slack.eithernet.ApiResult +import com.slack.eithernet.successOrElse +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.withContext + +class FriendsRepositoryImpl( + private val service: FriendsService, + private val dao: UsersDao +) : FriendsRepository { + + override suspend fun getAllFriends( + count: Int?, + offset: Int? + ): ApiResult = withContext(Dispatchers.IO) { + val friends = async { getFriends(count, offset) }.await() + .successOrElse { failure -> + return@withContext failure + } + + val onlineFriends = async { getOnlineFriends(count, offset) }.await() + .successOrElse { failure -> + return@withContext failure + }.mapNotNull { userId -> friends.find { it.id == userId } } + + ApiResult.success(FriendsInfo(friends, onlineFriends)) + } + + override suspend fun getFriends( + count: Int?, + offset: Int? + ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { + val requestModel = GetFriendsRequest( + order = "hints", + count = count, + offset = offset, + fields = VkConstants.USER_FIELDS + ) + service.getFriends(requestModel.map).mapApiResult( + successMapper = { apiResponse -> + val response = apiResponse.requireResponse() + val users = response.items.map(VkUserData::mapToDomain) + + VkMemoryCache.appendUsers(users) + + users + }, + errorMapper = { error -> error?.toDomain() } + ) + } + + override suspend fun getOnlineFriends( + count: Int?, + offset: Int? + ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { + val requestModel = GetOnlineFriendsRequest( + order = "hints", + count = count, + offset = offset, + ) + + service.getOnlineFriends(requestModel.map).mapApiDefault() + } + + override suspend fun storeUsers(users: List) = withContext(Dispatchers.IO) { + dao.insertAll(users.map(VkUser::asEntity)) + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsUseCase.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsUseCase.kt new file mode 100644 index 00000000..90749324 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/friends/FriendsUseCase.kt @@ -0,0 +1,26 @@ +package com.meloda.app.fast.data.api.friends + +import com.meloda.app.fast.data.State +import com.meloda.app.fast.model.FriendsInfo +import com.meloda.app.fast.model.api.domain.VkUser +import kotlinx.coroutines.flow.Flow + +interface FriendsUseCase { + + fun getAllFriends( + count: Int?, + offset: Int? + ): Flow> + + fun getFriends( + count: Int?, + offset: Int? + ): Flow>> + + fun getOnlineFriends( + count: Int?, + offset: Int? + ): Flow>> + + suspend fun storeUsers(users: List) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/longpoll/LongPollRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/longpoll/LongPollRepository.kt new file mode 100644 index 00000000..16033e7c --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/longpoll/LongPollRepository.kt @@ -0,0 +1,24 @@ +package com.meloda.app.fast.data.api.longpoll + +import com.meloda.app.fast.model.api.data.LongPollUpdates +import com.meloda.app.fast.model.api.data.VkLongPollData +import com.meloda.app.fast.network.RestApiErrorDomain +import com.slack.eithernet.ApiResult + +interface LongPollRepository { + + suspend fun getLongPollServer( + needPts: Boolean, + version: Int + ): ApiResult + + suspend fun getLongPollUpdates( + serverUrl: String, + act: String, + key: String, + ts: Int, + wait: Int, + mode: Int, + version: Int + ): ApiResult +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/longpoll/LongPollRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/longpoll/LongPollRepositoryImpl.kt new file mode 100644 index 00000000..ff054f75 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/longpoll/LongPollRepositoryImpl.kt @@ -0,0 +1,57 @@ +package com.meloda.app.fast.data.api.longpoll + +import com.meloda.app.fast.model.api.data.LongPollUpdates +import com.meloda.app.fast.model.api.data.VkLongPollData +import com.meloda.app.fast.model.api.requests.LongPollGetUpdatesRequest +import com.meloda.app.fast.model.api.requests.MessagesGetLongPollServerRequest +import com.meloda.app.fast.network.RestApiErrorDomain +import com.meloda.app.fast.network.mapApiResult +import com.meloda.app.fast.network.mapResult +import com.meloda.app.fast.network.service.longpoll.LongPollService +import com.meloda.app.fast.network.service.messages.MessagesService +import com.slack.eithernet.ApiResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class LongPollRepositoryImpl( + private val longPollService: LongPollService, + private val messagesService: MessagesService +) : LongPollRepository { + + override suspend fun getLongPollServer( + needPts: Boolean, + version: Int + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = MessagesGetLongPollServerRequest( + needPts = needPts, + version = version + ) + messagesService.getLongPollServer(requestModel.map).mapApiResult( + successMapper = { response -> response.requireResponse() }, + errorMapper = { error -> error?.toDomain() } + ) + } + + override suspend fun getLongPollUpdates( + serverUrl: String, + act: String, + key: String, + ts: Int, + wait: Int, + mode: Int, + version: Int + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = LongPollGetUpdatesRequest( + act = act, + key = key, + ts = ts, + wait = wait, + mode = mode, + version = version + ) + longPollService.getResponse(serverUrl, requestModel.map).mapResult( + successMapper = { response -> response }, + errorMapper = { error -> error?.toDomain() } + ) + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSource.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSource.kt new file mode 100644 index 00000000..9d3cd7ff --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSource.kt @@ -0,0 +1,16 @@ +package com.meloda.app.fast.data.api.messages + +import com.meloda.app.fast.model.database.VkMessageEntity + +interface MessagesLocalDataSource { + + suspend fun getMessages( + conversationId: Int, + offset: Int?, + count: Int? + ): List + + suspend fun getMessage(messageId: Int): VkMessageEntity? + + suspend fun storeMessages(messages: List) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSourceImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSourceImpl.kt new file mode 100644 index 00000000..c60632f0 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesLocalDataSourceImpl.kt @@ -0,0 +1,24 @@ +package com.meloda.app.fast.data.api.messages + +import com.meloda.app.fast.database.dao.MessageDao +import com.meloda.app.fast.model.database.VkMessageEntity + +// TODO: 05/05/2024, Danil Nikolaev: use paging for room +class MessagesLocalDataSourceImpl( + private val messageDao: MessageDao +) : MessagesLocalDataSource { + + override suspend fun getMessages( + conversationId: Int, + offset: Int?, + count: Int? + ): List = messageDao.getAll(conversationId) + + override suspend fun getMessage( + messageId: Int + ): VkMessageEntity? = messageDao.getById(messageId) + + override suspend fun storeMessages(messages: List) { + messageDao.insertAll(messages) + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSource.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSource.kt new file mode 100644 index 00000000..2ff8faa4 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSource.kt @@ -0,0 +1,36 @@ +package com.meloda.app.fast.data.api.messages + +import com.meloda.app.fast.model.api.domain.VkAttachment +import com.meloda.app.fast.model.api.domain.VkMessage +import com.meloda.app.fast.network.RestApiErrorDomain +import com.slack.eithernet.ApiResult + +interface MessagesNetworkDataSource { + + suspend fun getMessagesHistory( + conversationId: Int, + offset: Int?, + count: Int?, + ): ApiResult + + suspend fun getMessageById( + messagesIds: List, + extended: Boolean?, + fields: String? + ): ApiResult + + suspend fun send( + peerId: Int, + randomId: Int, + message: String?, + replyTo: Int?, + attachments: List? + ): ApiResult + + suspend fun markAsRead( + peerId: Int, + startMessageId: Int? + ): ApiResult + + suspend fun getMessage(messageId: Int): VkMessage? +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSourceImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSourceImpl.kt new file mode 100644 index 00000000..2f009484 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesNetworkDataSourceImpl.kt @@ -0,0 +1,164 @@ +package com.meloda.app.fast.data.api.messages + +import com.meloda.app.fast.common.VkConstants +import com.meloda.app.fast.data.VkGroupsMap +import com.meloda.app.fast.data.VkMemoryCache +import com.meloda.app.fast.data.VkUsersMap +import com.meloda.app.fast.model.api.data.VkContactData +import com.meloda.app.fast.model.api.data.VkGroupData +import com.meloda.app.fast.model.api.data.VkUserData +import com.meloda.app.fast.model.api.data.asDomain +import com.meloda.app.fast.model.api.domain.VkAttachment +import com.meloda.app.fast.model.api.domain.VkConversation +import com.meloda.app.fast.model.api.domain.VkMessage +import com.meloda.app.fast.model.api.requests.MessagesGetByIdRequest +import com.meloda.app.fast.model.api.requests.MessagesGetHistoryRequest +import com.meloda.app.fast.model.api.requests.MessagesMarkAsReadRequest +import com.meloda.app.fast.model.api.requests.MessagesSendRequest +import com.meloda.app.fast.network.RestApiErrorDomain +import com.meloda.app.fast.network.mapApiDefault +import com.meloda.app.fast.network.mapApiResult +import com.meloda.app.fast.network.service.messages.MessagesService +import com.slack.eithernet.ApiResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class MessagesNetworkDataSourceImpl( + private val messagesService: MessagesService +) : MessagesNetworkDataSource { + + override suspend fun getMessagesHistory( + conversationId: Int, + offset: Int?, + count: Int? + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = MessagesGetHistoryRequest( + count = count, + offset = offset, + peerId = conversationId, + extended = true, + startMessageId = null, + rev = null, + fields = VkConstants.ALL_FIELDS + ) + + messagesService.getHistory(requestModel.map).mapApiResult( + successMapper = { apiResponse -> + val response = apiResponse.requireResponse() + + val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain) + val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain) + val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain) + + val usersMap = VkUsersMap.forUsers(profilesList) + val groupsMap = VkGroupsMap.forGroups(groupsList) + + VkMemoryCache.appendUsers(profilesList) + VkMemoryCache.appendGroups(groupsList) + VkMemoryCache.appendContacts(contactsList) + + val messages = response.items.map { item -> + item.asDomain().let { message -> + message.copy( + user = usersMap.messageUser(message), + group = groupsMap.messageGroup(message), + actionUser = usersMap.messageActionUser(message), + actionGroup = groupsMap.messageActionGroup(message) + ).also { VkMemoryCache[message.id] = it } + } + } + + val conversations = response.conversations.orEmpty().map { item -> + val message = messages.firstOrNull { it.id == item.lastMessageId } + item.asDomain(message) + .let { conversation -> + conversation.copy( + user = usersMap.conversationUser(conversation), + group = groupsMap.conversationGroup(conversation) + ).also { VkMemoryCache[conversation.id] = it } + } + } + + MessagesHistoryDomain( + messages = messages, + conversations = conversations + ) + }, + errorMapper = { error -> + error?.toDomain() + } + ) + } + + override suspend fun getMessageById( + messagesIds: List, + extended: Boolean?, + fields: String? + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = MessagesGetByIdRequest( + messagesIds = messagesIds, + extended = extended, + fields = fields + ) + + messagesService.getById(requestModel.map).mapApiResult( + successMapper = { apiResponse -> + val response = apiResponse.requireResponse() + + val message = response.items.single() + val usersMap = + VkUsersMap.forUsers(response.profiles.orEmpty().map(VkUserData::mapToDomain)) + val groupsMap = + VkGroupsMap.forGroups(response.groups.orEmpty().map(VkGroupData::mapToDomain)) + + message.asDomain().copy( + user = usersMap.messageUser(message), + group = groupsMap.messageGroup(message), + actionUser = usersMap.messageActionUser(message), + actionGroup = groupsMap.messageActionGroup(message) + ) + }, + errorMapper = { error -> error?.toDomain() } + ) + } + + override suspend fun send( + peerId: Int, + randomId: Int, + message: String?, + replyTo: Int?, + attachments: List? + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = MessagesSendRequest( + peerId = peerId, + randomId = randomId, + message = message, + replyTo = replyTo, + attachments = attachments + ) + + messagesService.send(requestModel.map).mapApiDefault() + } + + override suspend fun markAsRead( + peerId: Int, + startMessageId: Int? + ): ApiResult = withContext(Dispatchers.IO) { + val requestModel = MessagesMarkAsReadRequest( + peerId = peerId, + startMessageId = startMessageId + ) + + messagesService.markAsRead(requestModel.map).mapApiDefault() + } + + override suspend fun getMessage(messageId: Int): VkMessage? = withContext(Dispatchers.IO) { + // TODO: 05/05/2024, Danil Nikolaev: get message + null + } +} + +data class MessagesHistoryDomain( + val messages: List, + val conversations: List +) diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepository.kt new file mode 100644 index 00000000..71b421f8 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepository.kt @@ -0,0 +1,75 @@ +package com.meloda.app.fast.data.api.messages + +import com.meloda.app.fast.model.api.domain.VkAttachment +import com.meloda.app.fast.model.api.domain.VkMessage +import com.meloda.app.fast.network.RestApiErrorDomain +import com.slack.eithernet.ApiResult +import kotlinx.coroutines.flow.Flow + +interface MessagesRepository { + + suspend fun getMessagesHistory( + conversationId: Int, + offset: Int?, + count: Int? + ): ApiResult + + suspend fun getMessageById( + messagesIds: List, + extended: Boolean?, + fields: String? + ): ApiResult + + suspend fun send( + peerId: Int, + randomId: Int, + message: String?, + replyTo: Int?, + attachments: List? + ): ApiResult + + suspend fun markAsRead( + peerId: Int, + startMessageId: Int? + ): ApiResult + + suspend fun getMessage(messageId: Int): Flow + + suspend fun storeMessages(messages: List) + +// suspend fun getHistory( +// params: MessagesGetHistoryRequest +// ): ApiResult + +// suspend fun markAsImportant( +// params: MessagesMarkAsImportantRequest +// ): ApiResult, RestApiErrorDomain> +// +// suspend fun pin( +// params: MessagesPinMessageRequest +// ): ApiResult +// +// suspend fun unpin( +// params: MessagesUnPinMessageRequest +// ): ApiResult +// +// suspend fun delete( +// params: MessagesDeleteRequest +// ): ApiResult +// +// suspend fun edit( +// params: MessagesEditRequest +// ): ApiResult +// +// suspend fun getChat( +// params: MessagesGetChatRequest +// ): ApiResult +// +// suspend fun getConversationMembers( +// params: MessagesGetConversationMembersRequest +// ): ApiResult +// +// suspend fun removeChatUser( +// params: MessagesRemoveChatUserRequest +// ): ApiResult +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepositoryImpl.kt new file mode 100644 index 00000000..4eb00f8d --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesRepositoryImpl.kt @@ -0,0 +1,201 @@ +package com.meloda.app.fast.data.api.messages + +import com.meloda.app.fast.model.api.domain.VkAttachment +import com.meloda.app.fast.model.api.domain.VkMessage +import com.meloda.app.fast.model.api.domain.asEntity +import com.meloda.app.fast.model.database.asExternalModel +import com.meloda.app.fast.network.RestApiErrorDomain +import com.slack.eithernet.ApiResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.withContext + +// TODO: 05/05/2024, Danil Nikolaev: implement syncing +class MessagesRepositoryImpl( + private val networkDataSource: MessagesNetworkDataSource, + private val localDataSource: MessagesLocalDataSource +) : MessagesRepository { + + override suspend fun getMessagesHistory( + conversationId: Int, + offset: Int?, + count: Int? + ): ApiResult = withContext(Dispatchers.IO) { +// val localMessages = localDataSource.getMessages( +// conversationId = conversationId, +// offset = offset, +// count = count +// ).map(VkMessageEntity::asExternalModel) +// +// emit(localMessages) +// +// val networkMessages = networkDataSource.getMessagesHistory( +// conversationId = conversationId, +// offset = offset, +// count = count +// ) +// +// emit(networkMessages) + + networkDataSource.getMessagesHistory(conversationId, offset, count) + } + + override suspend fun getMessageById( + messagesIds: List, + extended: Boolean?, + fields: String? + ): ApiResult = withContext(Dispatchers.IO) { + networkDataSource.getMessageById( + messagesIds = messagesIds, + extended = extended, + fields = fields + ) + } + + override suspend fun send( + peerId: Int, + randomId: Int, + message: String?, + replyTo: Int?, + attachments: List? + ): ApiResult = withContext(Dispatchers.IO) { + networkDataSource.send( + peerId, + randomId, + message, + replyTo, + attachments + ) + } + + override suspend fun markAsRead( + peerId: Int, + startMessageId: Int? + ): ApiResult = withContext(Dispatchers.IO) { + networkDataSource.markAsRead(peerId, startMessageId) + } + + override suspend fun getMessage(messageId: Int): Flow = flow { + val localMessage = localDataSource.getMessage(messageId)?.asExternalModel() + + emit(localMessage) + + val networkMessage = networkDataSource.getMessage(messageId) + + emit(networkMessage) + } + + override suspend fun storeMessages(messages: List) { + localDataSource.storeMessages(messages.map(VkMessage::asEntity)) + } + + // override suspend fun getHistory( +// params: MessagesGetHistoryRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.getHistory(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun send( +// params: MessagesSendRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.send(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun markAsImportant( +// params: MessagesMarkAsImportantRequest +// ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { +// messagesService.markAsImportant(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun pin( +// params: MessagesPinMessageRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.pin(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun unpin( +// params: MessagesUnPinMessageRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.unpin(params.map).mapResult( +// successMapper = {}, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun delete( +// params: MessagesDeleteRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.delete(params.map).mapResult( +// successMapper = {}, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun edit( +// params: MessagesEditRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.edit(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun getById( +// params: MessagesGetByIdRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.getById(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun markAsRead( +// params: MessagesMarkAsReadRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.markAsRead(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun getChat( +// params: MessagesGetChatRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.getChat(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun getConversationMembers( +// params: MessagesGetConversationMembersRequest +// ): ApiResult = +// withContext(Dispatchers.IO) { +// messagesService.getConversationMembers(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +// +// override suspend fun removeChatUser( +// params: MessagesRemoveChatUserRequest +// ): ApiResult = withContext(Dispatchers.IO) { +// messagesService.removeChatUser(params.map).mapResult( +// successMapper = { response -> response.requireResponse() }, +// errorMapper = { error -> error?.toDomain() } +// ) +// } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesUseCase.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesUseCase.kt new file mode 100644 index 00000000..14435ebc --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/messages/MessagesUseCase.kt @@ -0,0 +1,43 @@ +package com.meloda.app.fast.data.api.messages + +import com.meloda.app.fast.data.State +import com.meloda.app.fast.model.api.domain.VkAttachment +import com.meloda.app.fast.model.api.domain.VkMessage +import kotlinx.coroutines.flow.Flow + +interface MessagesUseCase { + + fun getMessagesHistory( + conversationId: Int, + count: Int?, + offset: Int? + ): Flow> + + fun getById( + messageId: Int, + extended: Boolean?, + fields: String? + ): Flow> + + fun getByIds( + messageIds: List, + extended: Boolean?, + fields: String? + ): Flow>> + + fun sendMessage( + peerId: Int, + randomId: Int, + message: String?, + replyTo: Int?, + attachments: List? + ): Flow> + + fun markAsRead( + peerId: Int, + startMessageId: Int + ): Flow> + + suspend fun storeMessage(message: VkMessage) + suspend fun storeMessages(messages: List) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepository.kt new file mode 100644 index 00000000..bb461775 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepository.kt @@ -0,0 +1,15 @@ +package com.meloda.app.fast.data.api.oauth + +import com.meloda.app.fast.model.api.responses.AuthDirectResponse + +interface OAuthRepository { + + suspend fun auth( + login: String, + password: String, + forceSms: Boolean, + twoFaCode: String?, + captchaSid: String?, + captchaKey: String? + ): AuthDirectResponse +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepositoryImpl.kt new file mode 100644 index 00000000..eee89074 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/oauth/OAuthRepositoryImpl.kt @@ -0,0 +1,51 @@ +package com.meloda.app.fast.data.api.oauth + +import com.meloda.app.fast.common.VkConstants +import com.meloda.app.fast.model.api.requests.AuthDirectRequest +import com.meloda.app.fast.model.api.responses.AuthDirectResponse +import com.meloda.app.fast.network.service.oauth.OAuthService +import com.slack.eithernet.ApiResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class OAuthRepositoryImpl( + private val oAuthService: OAuthService, +) : OAuthRepository { + + override suspend fun auth( + login: String, + password: String, + forceSms: Boolean, + twoFaCode: String?, + captchaSid: String?, + captchaKey: String? + ): AuthDirectResponse = withContext(Dispatchers.IO) { + val requestModel = AuthDirectRequest( + grantType = VkConstants.Auth.GrantType.PASSWORD, + clientId = VkConstants.VK_APP_ID, + clientSecret = VkConstants.VK_SECRET, + username = login, + password = password, + scope = VkConstants.Auth.SCOPE, + twoFaForceSms = forceSms, + twoFaCode = twoFaCode, + captchaSid = captchaSid, + captchaKey = captchaKey, + ) + + when (val result = oAuthService.auth(requestModel.map)) { + is ApiResult.Success -> result.value + + is ApiResult.Failure.HttpFailure -> { + requireNotNull(result.error) + } + + else -> throw IllegalStateException("Unknown result") + +// is ApiResult.Failure.ApiFailure -> TODO() +// is ApiResult.Failure.HttpFailure -> TODO() +// is ApiResult.Failure.NetworkFailure -> TODO() +// is ApiResult.Failure.UnknownFailure -> TODO() + } + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/photos/PhotosRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/photos/PhotosRepository.kt new file mode 100644 index 00000000..02ea7544 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/photos/PhotosRepository.kt @@ -0,0 +1,19 @@ +package com.meloda.app.fast.data.api.photos + +import com.meloda.app.fast.model.api.requests.PhotosSaveMessagePhotoRequest +import com.meloda.app.fast.network.service.photos.PhotosService +import okhttp3.MultipartBody + +class PhotosRepository( + private val photosService: PhotosService +) { + + suspend fun getMessagesUploadServer(peerId: Int) = + photosService.getUploadServer(mapOf("peer_id" to peerId.toString())) + + suspend fun uploadPhoto(url: String, photo: MultipartBody.Part) = + photosService.upload(url, photo) + + suspend fun saveMessagePhoto(body: PhotosSaveMessagePhotoRequest) = + photosService.save(body.map) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepository.kt new file mode 100644 index 00000000..2430593e --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepository.kt @@ -0,0 +1,8 @@ +package com.meloda.app.fast.data.api.users + +import com.meloda.app.fast.model.api.data.VkUserData +import com.meloda.app.fast.model.api.requests.UsersGetRequest + +interface UsersRepository { + suspend fun getById(params: UsersGetRequest): List +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepositoryImpl.kt new file mode 100644 index 00000000..b6ce26fe --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersRepositoryImpl.kt @@ -0,0 +1,16 @@ +package com.meloda.app.fast.data.api.users + +import com.meloda.app.fast.model.api.data.VkUserData +import com.meloda.app.fast.model.api.requests.UsersGetRequest +import com.meloda.app.fast.network.service.users.UsersService + +class UsersRepositoryImpl( + private val usersService: UsersService +) : UsersRepository { + + override suspend fun getById(params: UsersGetRequest): List { + // TODO: 05/05/2024, Danil Nikolaev: implement + + return emptyList() + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCase.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCase.kt new file mode 100644 index 00000000..7a47aa06 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCase.kt @@ -0,0 +1,23 @@ +package com.meloda.app.fast.data.api.users + +import com.meloda.app.fast.data.State +import com.meloda.app.fast.model.api.domain.VkUser +import kotlinx.coroutines.flow.Flow + +interface UsersUseCase { + + fun getUserById( + userId: Int, + fields: String?, + nomCase: String? + ): Flow> + + fun getUsersByIds( + userIds: List, + fields: String?, + nomCase: String? + ): Flow>> + + suspend fun storeUser(user: VkUser) + suspend fun storeUsers(users: List) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCaseImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCaseImpl.kt new file mode 100644 index 00000000..ca274362 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/users/UsersUseCaseImpl.kt @@ -0,0 +1,67 @@ +package com.meloda.app.fast.data.api.users + +import com.meloda.app.fast.data.State +import com.meloda.app.fast.model.api.domain.VkUser +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + + +// TODO: 05/05/2024, Danil Nikolaev: implement +class UsersUseCaseImpl( + private val usersRepository: UsersRepository, +) : UsersUseCase { + + override fun getUserById( + userId: Int, + fields: String?, + nomCase: String? + ): Flow> = flow { +// emit(State.Loading) +// +// val newState = usersRepository.getById( +// UsersGetRequest( +// userIds = listOf(userId), +// fields = fields, +// nomCase = nomCase +// ) +// ).fold( +// onSuccess = { response -> State.Success(response.singleOrNull()?.mapToDomain()) }, +// onNetworkFailure = { State.Error.ConnectionError }, +// onUnknownFailure = { State.UNKNOWN_ERROR }, +// onHttpFailure = { result -> result.error.toStateApiError() }, +// onApiFailure = { result -> result.error.toStateApiError() } +// ) +// emit(newState) + } + + override fun getUsersByIds( + userIds: List, + fields: String?, + nomCase: String? + ): Flow>> = flow { +// emit(State.Loading) +// +// val newState = usersRepository.getById( +// UsersGetRequest( +// userIds = userIds, +// fields = fields, +// nomCase = nomCase +// ) +// ).fold( +// onSuccess = { response -> State.Success(response.map(VkUserData::mapToDomain)) }, +// onNetworkFailure = { State.Error.ConnectionError }, +// onUnknownFailure = { State.UNKNOWN_ERROR }, +// onHttpFailure = { result -> result.error.toStateApiError() }, +// onApiFailure = { result -> result.error.toStateApiError() } +// ) +// emit(newState) + } + + override suspend fun storeUser(user: VkUser) { + + } + + override suspend fun storeUsers(users: List) { + + } +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/api/videos/VideosRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/videos/VideosRepository.kt new file mode 100644 index 00000000..ecb7b178 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/api/videos/VideosRepository.kt @@ -0,0 +1,14 @@ +package com.meloda.app.fast.data.api.videos + +import com.meloda.app.fast.network.service.videos.VideosService +import okhttp3.MultipartBody + +class VideosRepository( + private val videosService: VideosService +) { + + suspend fun save() = videosService.save() + + // TODO: 05/05/2024, Danil Nikolaev: research, maybe remove multipart.body + suspend fun upload(url: String, file: MultipartBody.Part) = videosService.upload(url, file) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepository.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepository.kt new file mode 100644 index 00000000..18074eef --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepository.kt @@ -0,0 +1,10 @@ +package com.meloda.app.fast.data.db + +import com.meloda.app.fast.model.database.AccountEntity + +interface AccountsRepository { + + suspend fun getAccounts(): List + + suspend fun storeAccounts(accounts: List) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepositoryImpl.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepositoryImpl.kt new file mode 100644 index 00000000..81579668 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/db/AccountsRepositoryImpl.kt @@ -0,0 +1,15 @@ +package com.meloda.app.fast.data.db + +import com.meloda.app.fast.database.dao.AccountDao +import com.meloda.app.fast.model.database.AccountEntity + +class AccountsRepositoryImpl( + private val accountDao: AccountDao +) : AccountsRepository { + + override suspend fun getAccounts(): List = accountDao.getAll() + + override suspend fun storeAccounts( + accounts: List + ) = accountDao.insertAll(accounts) +} diff --git a/core/data/src/main/kotlin/com/meloda/app/fast/data/di/DataModule.kt b/core/data/src/main/kotlin/com/meloda/app/fast/data/di/DataModule.kt new file mode 100644 index 00000000..76d1c4d1 --- /dev/null +++ b/core/data/src/main/kotlin/com/meloda/app/fast/data/di/DataModule.kt @@ -0,0 +1,78 @@ +package com.meloda.app.fast.data.di + +import com.meloda.app.fast.common.di.commonModule +import com.meloda.app.fast.data.api.account.AccountRepository +import com.meloda.app.fast.data.api.account.AccountRepositoryImpl +import com.meloda.app.fast.data.api.account.AccountUseCase +import com.meloda.app.fast.data.api.account.AccountUseCaseImpl +import com.meloda.app.fast.data.api.audios.AudiosRepository +import com.meloda.app.fast.data.api.auth.AuthRepository +import com.meloda.app.fast.data.api.auth.AuthRepositoryImpl +import com.meloda.app.fast.data.api.conversations.ConversationsRepository +import com.meloda.app.fast.data.api.conversations.ConversationsRepositoryImpl +import com.meloda.app.fast.data.api.files.FilesRepository +import com.meloda.app.fast.data.api.friends.FriendsRepository +import com.meloda.app.fast.data.api.friends.FriendsRepositoryImpl +import com.meloda.app.fast.data.api.longpoll.LongPollRepository +import com.meloda.app.fast.data.api.longpoll.LongPollRepositoryImpl +import com.meloda.app.fast.data.api.messages.MessagesLocalDataSource +import com.meloda.app.fast.data.api.messages.MessagesLocalDataSourceImpl +import com.meloda.app.fast.data.api.messages.MessagesNetworkDataSource +import com.meloda.app.fast.data.api.messages.MessagesNetworkDataSourceImpl +import com.meloda.app.fast.data.api.messages.MessagesRepository +import com.meloda.app.fast.data.api.messages.MessagesRepositoryImpl +import com.meloda.app.fast.data.api.oauth.OAuthRepository +import com.meloda.app.fast.data.api.oauth.OAuthRepositoryImpl +import com.meloda.app.fast.data.api.photos.PhotosRepository +import com.meloda.app.fast.data.api.users.UsersRepository +import com.meloda.app.fast.data.api.users.UsersRepositoryImpl +import com.meloda.app.fast.data.api.users.UsersUseCase +import com.meloda.app.fast.data.api.users.UsersUseCaseImpl +import com.meloda.app.fast.data.api.videos.VideosRepository +import com.meloda.app.fast.data.db.AccountsRepository +import com.meloda.app.fast.data.db.AccountsRepositoryImpl +import com.meloda.app.fast.database.di.databaseModule +import com.meloda.app.fast.datastore.di.dataStoreModule +import com.meloda.app.fast.network.di.networkModule +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.bind +import org.koin.dsl.module + +val dataModule = module { + includes( + commonModule, + databaseModule, + dataStoreModule, + networkModule, + ) + + singleOf(::AccountRepositoryImpl) bind AccountRepository::class + singleOf(::AccountUseCaseImpl) bind AccountUseCase::class + + singleOf(::AudiosRepository) + + singleOf(::AuthRepositoryImpl) bind AuthRepository::class + + singleOf(::ConversationsRepositoryImpl) bind ConversationsRepository::class + + singleOf(::FilesRepository) + + singleOf(::LongPollRepositoryImpl) bind LongPollRepository::class + + singleOf(::MessagesLocalDataSourceImpl) bind MessagesLocalDataSource::class + singleOf(::MessagesNetworkDataSourceImpl) bind MessagesNetworkDataSource::class + singleOf(::MessagesRepositoryImpl) bind MessagesRepository::class + + singleOf(::OAuthRepositoryImpl) bind OAuthRepository::class + + singleOf(::PhotosRepository) + + singleOf(::UsersRepositoryImpl) bind UsersRepository::class + singleOf(::UsersUseCaseImpl) bind UsersUseCase::class + + singleOf(::VideosRepository) + + singleOf(::AccountsRepositoryImpl) bind AccountsRepository::class + + singleOf(::FriendsRepositoryImpl) bind FriendsRepository::class +} diff --git a/core/database/.gitignore b/core/database/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/database/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts new file mode 100644 index 00000000..bc87cf96 --- /dev/null +++ b/core/database/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.org.jetbrains.kotlin.android) + alias(libs.plugins.com.google.devtools.ksp) +} + +group = "com.meloda.app.fast.database" + +android { + namespace = "com.meloda.app.fast.database" + compileSdk = Configs.compileSdk + + defaultConfig { + minSdk = Configs.minSdk + } + + ksp { + arg("room.schemaLocation", "$projectDir/schemas") + arg("room.generateKotlin", "true") + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = Configs.java + targetCompatibility = Configs.java + } + kotlinOptions { + jvmTarget = Configs.java.toString() + } +} + +dependencies { + api(projects.core.model) + + implementation(libs.room.ktx) + implementation(libs.room.runtime) + ksp(libs.room.compiler) + + implementation(libs.koin.android) +} diff --git a/core/database/src/main/AndroidManifest.xml b/core/database/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/core/database/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/AccountsDatabase.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/AccountsDatabase.kt new file mode 100644 index 00000000..ec7a0a67 --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/AccountsDatabase.kt @@ -0,0 +1,16 @@ +package com.meloda.app.fast.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.meloda.app.fast.database.dao.AccountDao +import com.meloda.app.fast.model.database.AccountEntity + +@Database( + entities = [AccountEntity::class], + version = 2 +) +abstract class AccountsDatabase : RoomDatabase() { + abstract fun accountDao(): AccountDao +} + + diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/CacheDatabase.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/CacheDatabase.kt new file mode 100644 index 00000000..35521021 --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/CacheDatabase.kt @@ -0,0 +1,32 @@ +package com.meloda.app.fast.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.meloda.app.fast.database.dao.ConversationDao +import com.meloda.app.fast.database.dao.GroupDao +import com.meloda.app.fast.database.dao.MessageDao +import com.meloda.app.fast.database.dao.UsersDao +import com.meloda.app.fast.database.typeconverters.Converters +import com.meloda.app.fast.model.database.VkConversationEntity +import com.meloda.app.fast.model.database.VkGroupEntity +import com.meloda.app.fast.model.database.VkMessageEntity +import com.meloda.app.fast.model.database.VkUserEntity + +@Database( + entities = [ + VkUserEntity::class, + VkGroupEntity::class, + VkMessageEntity::class, + VkConversationEntity::class + ], + + version = 5 +) +@TypeConverters(Converters::class) +abstract class CacheDatabase : RoomDatabase() { + abstract fun userDao(): UsersDao + abstract fun groupDao(): GroupDao + abstract fun messageDao(): MessageDao + abstract fun conversationDao(): ConversationDao +} diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/AccountDao.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/AccountDao.kt new file mode 100644 index 00000000..c960accc --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/AccountDao.kt @@ -0,0 +1,15 @@ +package com.meloda.app.fast.database.dao + +import androidx.room.Dao +import androidx.room.Query +import com.meloda.app.fast.model.database.AccountEntity + +@Dao +abstract class AccountDao : EntityDao { + + @Query("SELECT * FROM accounts") + abstract suspend fun getAll(): List + + @Query("DELETE FROM accounts WHERE userId = :userId") + abstract suspend fun deleteById(userId: Int) +} diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/ConversationDao.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/ConversationDao.kt new file mode 100644 index 00000000..1124c8bd --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/ConversationDao.kt @@ -0,0 +1,30 @@ +package com.meloda.app.fast.database.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import com.meloda.app.fast.model.database.ConversationWithMessage +import com.meloda.app.fast.model.database.VkConversationEntity + +@Dao +abstract class ConversationDao : EntityDao { + + @Query("SELECT * FROM conversations") + abstract suspend fun getAll(): List + + @Query("SELECT * FROM conversations WHERE id IN (:ids)") + abstract suspend fun getAllByIds(ids: List): List + + @Query("SELECT * FROM conversations WHERE id IS (:id)") + abstract suspend fun getById(id: Int): VkConversationEntity? + + @Transaction + @Query("SELECT * FROM conversations WHERE id IS (:id)") + abstract suspend fun getByIdWithMessage(id: Int): ConversationWithMessage? + + @Query("DELETE FROM conversations WHERE rowid IN (:ids)") + abstract suspend fun deleteByIds(ids: List): Int +} + + + diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/EntityDao.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/EntityDao.kt new file mode 100644 index 00000000..a6ab07a0 --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/EntityDao.kt @@ -0,0 +1,20 @@ +package com.meloda.app.fast.database.dao + +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy + +interface EntityDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertAll(values: List) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(value: T) + + @Delete + suspend fun delete(value: T): Int + + @Delete + suspend fun deleteAll(values: List): Int +} diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/GroupDao.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/GroupDao.kt new file mode 100644 index 00000000..1fabe1ce --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/GroupDao.kt @@ -0,0 +1,18 @@ +package com.meloda.app.fast.database.dao + +import androidx.room.Dao +import androidx.room.Query +import com.meloda.app.fast.model.database.VkGroupEntity + +@Dao +abstract class GroupDao : EntityDao { + + @Query("SELECT * FROM groups") + abstract suspend fun getAll(): List + + @Query("SELECT * FROM groups WHERE id IN (:ids)") + abstract suspend fun getAllByIds(ids: List): List + + @Query("DELETE FROM groups WHERE id IN (:ids)") + abstract suspend fun deleteByIds(ids: List): Int +} diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/MessageDao.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/MessageDao.kt new file mode 100644 index 00000000..597c406f --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/MessageDao.kt @@ -0,0 +1,24 @@ +package com.meloda.app.fast.database.dao + +import androidx.room.Dao +import androidx.room.Query +import com.meloda.app.fast.model.database.VkMessageEntity + +@Dao +abstract class MessageDao : EntityDao { + + @Query("SELECT * FROM messages") + abstract suspend fun getAll(): List + + @Query("SELECT * FROM messages WHERE peerId IS (:conversationId)") + abstract suspend fun getAll(conversationId: Int): List + + @Query("SELECT * FROM messages WHERE id IN (:ids)") + abstract suspend fun getAllByIds(ids: List): List + + @Query("SELECT * FROM messages WHERE id IS (:messageId)") + abstract suspend fun getById(messageId: Int): VkMessageEntity? + + @Query("DELETE FROM messages WHERE id IN (:ids)") + abstract suspend fun deleteByIds(ids: List): Int +} diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/UsersDao.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/UsersDao.kt new file mode 100644 index 00000000..a9cd10f5 --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/dao/UsersDao.kt @@ -0,0 +1,18 @@ +package com.meloda.app.fast.database.dao + +import androidx.room.Dao +import androidx.room.Query +import com.meloda.app.fast.model.database.VkUserEntity + +@Dao +abstract class UsersDao : EntityDao { + + @Query("SELECT * FROM users") + abstract suspend fun getAll(): List + + @Query("SELECT * FROM users WHERE id IN (:ids)") + abstract suspend fun getAllByIds(ids: List): List + + @Query("DELETE FROM users WHERE id IN (:ids)") + abstract suspend fun deleteByIds(ids: List): Int +} diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/di/DatabaseModule.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/di/DatabaseModule.kt new file mode 100644 index 00000000..9cbd0a8a --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/di/DatabaseModule.kt @@ -0,0 +1,25 @@ +package com.meloda.app.fast.database.di + +import androidx.room.Room +import com.meloda.app.fast.database.AccountsDatabase +import org.koin.core.scope.Scope +import org.koin.dsl.module + +val databaseModule = module { + single { + Room.databaseBuilder(get(), AccountsDatabase::class.java, "accounts").build() + } + single { get().accountDao() } + + single { + Room.databaseBuilder(get(), com.meloda.app.fast.database.CacheDatabase::class.java, "cache") + .fallbackToDestructiveMigration() + .build() + } + single { cacheDB().userDao() } + single { cacheDB().groupDao() } + single { cacheDB().messageDao() } + single { cacheDB().conversationDao() } +} + +private fun Scope.cacheDB(): com.meloda.app.fast.database.CacheDatabase = get() diff --git a/core/database/src/main/kotlin/com/meloda/app/fast/database/typeconverters/Converters.kt b/core/database/src/main/kotlin/com/meloda/app/fast/database/typeconverters/Converters.kt new file mode 100644 index 00000000..791c769e --- /dev/null +++ b/core/database/src/main/kotlin/com/meloda/app/fast/database/typeconverters/Converters.kt @@ -0,0 +1,21 @@ +package com.meloda.app.fast.database.typeconverters + +import androidx.room.TypeConverter + +class Converters { + + @TypeConverter + fun intListToString(list: List): String = list.joinToString() + + @TypeConverter + fun stringToIntList(string: String): List = + string + .split(", ") + .mapNotNull(String::toIntOrNull) + + @TypeConverter + fun stringListToString(list: List): String = list.joinToString() + + @TypeConverter + fun stringToStringList(string: String): List = string.split(", ") +} diff --git a/core/datastore/.gitignore b/core/datastore/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/datastore/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts new file mode 100644 index 00000000..d85cce98 --- /dev/null +++ b/core/datastore/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.org.jetbrains.kotlin.android) +} + +group = "com.meloda.app.fast.datastore" + +android { + namespace = "com.meloda.app.fast.datastore" + compileSdk = Configs.compileSdk + + defaultConfig { + minSdk = Configs.minSdk + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = Configs.java + targetCompatibility = Configs.java + } + kotlinOptions { + jvmTarget = Configs.java.toString() + } +} + +dependencies { + api(projects.core.common) + + implementation(libs.koin.android) +} diff --git a/core/datastore/src/main/AndroidManifest.xml b/core/datastore/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/core/datastore/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/Extensions.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/Extensions.kt new file mode 100644 index 00000000..b070ebc5 --- /dev/null +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/Extensions.kt @@ -0,0 +1,57 @@ +package com.meloda.app.fast.datastore + +import android.content.res.Configuration +import android.content.res.Resources +import android.os.PowerManager +import androidx.appcompat.app.AppCompatDelegate + +fun isUsingDarkMode( + resources: Resources, + powerManager: PowerManager, +): Boolean { + val nightThemeMode: Int = SettingsController.getInt( + SettingsKeys.KEY_APPEARANCE_DARK_THEME, + SettingsKeys.DEFAULT_VALUE_APPEARANCE_DARK_THEME + ) + + val appForceDarkMode = nightThemeMode == AppCompatDelegate.MODE_NIGHT_YES + val appBatterySaver = nightThemeMode == AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY + + val systemUiNightMode = resources.configuration.uiMode + + val isSystemBatterySaver = powerManager.isPowerSaveMode + val isSystemUsingDarkTheme = + systemUiNightMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + + return appForceDarkMode || (appBatterySaver && isSystemBatterySaver) || (!appBatterySaver && isSystemUsingDarkTheme && nightThemeMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) +} + +fun isUsingDynamicColors(): Boolean = SettingsController.getBoolean( + SettingsKeys.KEY_USE_DYNAMIC_COLORS, + SettingsKeys.DEFAULT_VALUE_USE_DYNAMIC_COLORS +) + +fun isUsingAmoledBackground(): Boolean = SettingsController.getBoolean( + SettingsKeys.KEY_APPEARANCE_AMOLED_THEME, + SettingsKeys.DEFAULT_VALUE_APPEARANCE_AMOLED_THEME +) + +fun selectedColorScheme(): Int = SettingsController.getInt( + SettingsKeys.KEY_APPEARANCE_COLOR_SCHEME, + SettingsKeys.DEFAULT_VALUE_APPEARANCE_COLOR_SCHEME +) + +fun isUsingBlur(): Boolean = SettingsController.getBoolean( + SettingsKeys.KEY_APPEARANCE_BLUR, + SettingsKeys.DEFAULT_VALUE_KEY_APPEARANCE_BLUR +) + +fun isDebugSettingsShown(): Boolean = SettingsController.getBoolean( + SettingsKeys.KEY_SHOW_DEBUG_CATEGORY, + false +) + +fun isMultiline(): Boolean = SettingsController.getBoolean( + SettingsKeys.KEY_APPEARANCE_MULTILINE, + SettingsKeys.DEFAULT_VALUE_MULTILINE +) diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsController.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsController.kt new file mode 100644 index 00000000..8fc99016 --- /dev/null +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsController.kt @@ -0,0 +1,53 @@ +package com.meloda.app.fast.datastore + +import android.content.SharedPreferences +import androidx.core.content.edit +import kotlin.properties.Delegates + +object SettingsController { + + private var preferences: SharedPreferences by Delegates.notNull() + + fun init(preferences: SharedPreferences) { + this.preferences = preferences + } + + fun edit( + commit: Boolean = false, + action: SharedPreferences.Editor.() -> Unit + ) { + preferences.edit(commit, action) + } + + fun getString(key: String, defaultValue: String?): String? { + return preferences.getString(key, defaultValue) + } + + fun getBoolean(key: String, defaultValue: Boolean): Boolean { + return preferences.getBoolean(key, defaultValue) + } + + fun getInt(key: String, defaultValue: Int): Int { + return preferences.getInt(key, defaultValue) + } + + fun getLong(key: String, defaultValue: Long): Long { + return preferences.getLong(key, defaultValue) + } + + fun getFloat(key: String, defaultValue: Float): Float { + return preferences.getFloat(key, defaultValue) + } + + fun put(key: String, newValue: T?) { + preferences.edit { + when (newValue) { + is String -> putString(key, newValue) + is Boolean -> putBoolean(key, newValue) + is Int -> putInt(key, newValue) + is Long -> putLong(key, newValue) + is Float -> putFloat(key, newValue) + } + } + } +} diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsKeys.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsKeys.kt new file mode 100644 index 00000000..1f11ab5b --- /dev/null +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/SettingsKeys.kt @@ -0,0 +1,50 @@ +package com.meloda.app.fast.datastore + +import androidx.appcompat.app.AppCompatDelegate + +object SettingsKeys { + const val KEY_ACCOUNT = "account" + const val KEY_ACCOUNT_LOGOUT = "account_logout" + + const val KEY_GENERAL = "general" + const val KEY_USE_CONTACT_NAMES = "general_use_contact_names" + const val DEFAULT_VALUE_USE_CONTACT_NAMES = false + const val KEY_SHOW_EMOJI_BUTTON = "general_show_emoji_button" + const val DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON = false + + const val KEY_APPEARANCE = "appearance" + const val KEY_APPEARANCE_MULTILINE = "appearance_multiline" + const val DEFAULT_VALUE_MULTILINE = true + const val KEY_APPEARANCE_DARK_THEME = "appearance_appearance_dark_theme" + const val DEFAULT_VALUE_APPEARANCE_DARK_THEME = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + const val KEY_APPEARANCE_AMOLED_THEME = "appearance_amoled_theme" + const val DEFAULT_VALUE_APPEARANCE_AMOLED_THEME = false + const val KEY_USE_DYNAMIC_COLORS = "appearance_use_dynamic_colors" + const val DEFAULT_VALUE_USE_DYNAMIC_COLORS = false + const val KEY_APPEARANCE_COLOR_SCHEME = "appearance_color_scheme" + const val DEFAULT_VALUE_APPEARANCE_COLOR_SCHEME = 0 + const val KEY_APPEARANCE_LANGUAGE = "appearance_language" + const val KEY_APPEARANCE_BLUR = "appearance_blur" + const val DEFAULT_VALUE_KEY_APPEARANCE_BLUR = false + + const val KEY_FEATURES_HIDE_KEYBOARD_ON_SCROLL = "features_hide_keyboard_on_scroll" + const val KEY_FEATURES_FAST_TEXT = "features_fast_text" + const val DEFAULT_VALUE_FEATURES_FAST_TEXT = "¯\\_(ツ)_/¯" + const val KEY_FEATURES_LONG_POLL_IN_BACKGROUND = "features_lp_background" + const val DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND = false + + const val KEY_VISIBILITY_SEND_ONLINE_STATUS = "visibility_send_online_status" + const val DEFAULT_VALUE_KEY_VISIBILITY_SEND_ONLINE_STATUS = false + + const val KEY_DEBUG_PERFORM_CRASH = "debug_perform_crash" + const val KEY_DEBUG_SHOW_CRASH_ALERT = "debug_show_crash_alert" + const val KEY_DEBUG_HIDE_DEBUG_LIST = "debug_hide_debug_list" + const val KEY_SHOW_EXACT_TIME_ON_TIME_STAMP = "wip_show_exact_time_on_time_stamp" + const val KEY_SHOW_NAME_IN_BUBBLES = "debug_show_title_in_bubbles" + const val KEY_SHOW_DATE_UNDER_BUBBLES = "debug_show_date_under_bubbles" + const val KEY_ENABLE_ANIMATIONS_IN_MESSAGES = "debug_enable_animations_in_messages" + + const val KEY_SHOW_DEBUG_CATEGORY = "show_debug_category" + + const val ID_DMITRY = 37610580 +} diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/UserSettings.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/UserSettings.kt new file mode 100644 index 00000000..c5ff9f9d --- /dev/null +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/UserSettings.kt @@ -0,0 +1,120 @@ +package com.meloda.app.fast.datastore + +import android.content.res.Resources +import android.os.PowerManager +import com.meloda.app.fast.datastore.model.ThemeConfig +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +interface UserSettings { + val theme: StateFlow + val longPollBackground: StateFlow + val online: StateFlow + val debugSettingsEnabled: StateFlow + val useContactNames: StateFlow + + fun updateUsingDarkTheme() + fun useDarkThemeChanged(use: Boolean) + fun useAmoledThemeChanged(use: Boolean) + fun useDynamicColorsChanged(use: Boolean) + fun useBlurChanged(use: Boolean) + fun useMultiline(use: Boolean) + fun setLongPollBackground(background: Boolean) + fun setOnline(use: Boolean) + fun enableDebugSettings(enable: Boolean) + fun onUseContactNamesChanged(use: Boolean) +} + +class UserSettingsImpl( + private val resources: Resources, + private val powerManager: PowerManager +) : UserSettings { + + override val theme = MutableStateFlow( + ThemeConfig( + usingDarkStyle = isUsingDarkMode(resources, powerManager), + usingDynamicColors = isUsingDynamicColors(), + selectedColorScheme = selectedColorScheme(), + usingAmoledBackground = isUsingAmoledBackground(), + usingBlur = isUsingBlur(), + multiline = isMultiline() + ) + ) + + override val longPollBackground = MutableStateFlow( + SettingsController.getBoolean( + SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, + SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND + ) + ) + override val online = MutableStateFlow( + SettingsController.getBoolean( + SettingsKeys.KEY_VISIBILITY_SEND_ONLINE_STATUS, + SettingsKeys.DEFAULT_VALUE_KEY_VISIBILITY_SEND_ONLINE_STATUS + ) + ) + + override val debugSettingsEnabled = MutableStateFlow( + SettingsController.getBoolean( + SettingsKeys.KEY_SHOW_DEBUG_CATEGORY, + false + ) + ) + + override val useContactNames = MutableStateFlow( + SettingsController.getBoolean( + SettingsKeys.KEY_USE_CONTACT_NAMES, + SettingsKeys.DEFAULT_VALUE_USE_CONTACT_NAMES + ) + ) + + override fun updateUsingDarkTheme() { + useDarkThemeChanged( + isUsingDarkMode( + resources = resources, + powerManager = powerManager, + ) + ) + } + + override fun useDarkThemeChanged(use: Boolean) { + theme.value = theme.value.copy( + usingDarkStyle = use + ) + } + + override fun useAmoledThemeChanged(use: Boolean) { + theme.value = theme.value.copy( + usingAmoledBackground = use + ) + } + + override fun useDynamicColorsChanged(use: Boolean) { + theme.value = theme.value.copy(usingDynamicColors = use) + } + + override fun useBlurChanged(use: Boolean) { + theme.value = theme.value.copy(usingBlur = use) + } + + override fun useMultiline(use: Boolean) { + theme.value = theme.value.copy(multiline = use) + } + + override fun setLongPollBackground(background: Boolean) { + longPollBackground.value = background + } + + override fun setOnline(use: Boolean) { + online.value = use + } + + override fun enableDebugSettings(enable: Boolean) { + debugSettingsEnabled.update { enable } + } + + override fun onUseContactNamesChanged(use: Boolean) { + useContactNames.update { use } + } +} diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/di/DataStoreModule.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/di/DataStoreModule.kt new file mode 100644 index 00000000..7a595007 --- /dev/null +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/di/DataStoreModule.kt @@ -0,0 +1,11 @@ +package com.meloda.app.fast.datastore.di + +import com.meloda.app.fast.datastore.UserSettings +import com.meloda.app.fast.datastore.UserSettingsImpl +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.bind +import org.koin.dsl.module + +val dataStoreModule = module { + singleOf(::UserSettingsImpl) bind UserSettings::class +} diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/model/ThemeConfig.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/model/ThemeConfig.kt new file mode 100644 index 00000000..cc94c419 --- /dev/null +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/model/ThemeConfig.kt @@ -0,0 +1,11 @@ +package com.meloda.app.fast.datastore.model + +data class ThemeConfig( + val usingDarkStyle: Boolean, + val usingDynamicColors: Boolean, + val selectedColorScheme: Int, + val usingAmoledBackground: Boolean, + val usingBlur: Boolean, + val multiline: Boolean, + val bubblesWithPinch: Boolean = true +) diff --git a/core/designsystem/.gitignore b/core/designsystem/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core/designsystem/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts new file mode 100644 index 00000000..dc6d4af4 --- /dev/null +++ b/core/designsystem/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.org.jetbrains.kotlin.android) + alias(libs.plugins.kotlin.compose.compiler) +} + +group = "com.meloda.app.fast.designsystem" + +android { + namespace = "com.meloda.app.fast.designsystem" + + compileSdk = Configs.compileSdk + + defaultConfig { + minSdk = Configs.minSdk + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = Configs.java + targetCompatibility = Configs.java + } + kotlinOptions { + jvmTarget = Configs.java.toString() + } + buildFeatures { + compose = true + } + composeOptions { + useLiveLiterals = true + } +} + +dependencies { + // TODO: 05/05/2024, Danil Nikolaev: maybe remove + implementation(projects.core.common) + implementation(projects.core.datastore) + + implementation(libs.appcompat) + implementation(libs.accompanist.permissions) + + implementation(platform(libs.compose.bom)) + implementation(libs.bundles.compose) + + implementation(libs.haze) + implementation(libs.haze.materials) +} diff --git a/core/designsystem/src/main/AndroidManifest.xml b/core/designsystem/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/core/designsystem/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AppTheme.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AppTheme.kt new file mode 100644 index 00000000..7ec28bcc --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AppTheme.kt @@ -0,0 +1,178 @@ +package com.meloda.app.fast.designsystem + +import android.app.Activity +import android.os.Build +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.core.view.WindowCompat +import com.meloda.app.fast.datastore.isUsingAmoledBackground +import com.meloda.app.fast.datastore.isUsingDynamicColors +import com.meloda.app.fast.datastore.model.ThemeConfig +import com.meloda.app.fast.datastore.selectedColorScheme +import com.meloda.app.fast.designsystem.colorschemes.ClassicColorScheme + +private val googleSansFonts = FontFamily( + Font(resId = R.font.google_sans_regular), + Font( + resId = R.font.google_sans_italic, + style = FontStyle.Italic + ), + Font( + resId = R.font.google_sans_medium, + weight = FontWeight.Medium + ), + Font( + resId = R.font.google_sans_medium_italic, + weight = FontWeight.Medium, + style = FontStyle.Italic + ), + Font( + resId = R.font.google_sans_bold, + weight = FontWeight.Bold + ), + Font( + resId = R.font.google_sans_bold_italic, + weight = FontWeight.Bold, + style = FontStyle.Italic + ) +) + +private val robotoFonts = FontFamily( + Font( + resId = R.font.roboto_thin, + weight = FontWeight.Thin + ), + Font( + resId = R.font.roboto_thin_italic, + weight = FontWeight.Thin, + style = FontStyle.Italic + ), + Font( + resId = R.font.roboto_light, + weight = FontWeight.Light + ), + Font( + resId = R.font.roboto_light_italic, + weight = FontWeight.Light, + style = FontStyle.Italic + ), + Font(resId = R.font.roboto_regular), + Font( + resId = R.font.roboto_italic, + style = FontStyle.Italic + ), + Font( + resId = R.font.roboto_medium, + weight = FontWeight.Medium + ), + Font( + resId = R.font.roboto_medium_italic, + weight = FontWeight.Medium, + style = FontStyle.Italic + ), + Font( + resId = R.font.roboto_bold, + weight = FontWeight.Bold + ), + Font( + resId = R.font.roboto_bold_italic, + weight = FontWeight.Bold, + style = FontStyle.Italic + ), + Font( + resId = R.font.roboto_black, + weight = FontWeight.Black + ), + Font( + resId = R.font.roboto_black_italic, + weight = FontWeight.Black, + style = FontStyle.Italic + ) +) + +val LocalTheme = compositionLocalOf { + ThemeConfig( + usingDarkStyle = false, + usingDynamicColors = false, + selectedColorScheme = 0, + usingAmoledBackground = false, + usingBlur = false, + multiline = false + ) +} + +@Composable +fun AppTheme( + predefinedColorScheme: ColorScheme? = null, + useDarkTheme: Boolean = isUsingDarkTheme(), + useDynamicColors: Boolean = isUsingDynamicColors(), + selectedColorScheme: Int = selectedColorScheme(), + useAmoledBackground: Boolean = isUsingAmoledBackground(), + content: @Composable () -> Unit +) { + val colorScheme: ColorScheme = when { + useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (useDarkTheme) dynamicDarkColorScheme(context) + else dynamicLightColorScheme(context) + } + + else -> { + // TODO: 03/07/2024, Danil Nikolaev: add color picker to settings + when (selectedColorScheme) { + 1 -> if (useDarkTheme) darkColorScheme() else lightColorScheme() + else -> if (useDarkTheme) ClassicColorScheme.darkScheme else ClassicColorScheme.lightScheme + } + } + }.let { scheme -> + if (useDarkTheme && useAmoledBackground) { + scheme.copy( + background = Color.Black, + surface = Color.Black + ) + } else { + scheme + } + } + + val typography = MaterialTheme.typography.copy( + displayLarge = MaterialTheme.typography.displayLarge.copy(fontFamily = googleSansFonts), + displayMedium = MaterialTheme.typography.displayMedium.copy(fontFamily = googleSansFonts), + displaySmall = MaterialTheme.typography.displaySmall.copy(fontFamily = googleSansFonts), + headlineLarge = MaterialTheme.typography.headlineLarge.copy(fontFamily = googleSansFonts), + headlineMedium = MaterialTheme.typography.headlineMedium.copy(fontFamily = googleSansFonts), + headlineSmall = MaterialTheme.typography.headlineSmall.copy(fontFamily = googleSansFonts), + bodyLarge = MaterialTheme.typography.bodyLarge.copy(fontFamily = robotoFonts), + bodyMedium = MaterialTheme.typography.bodyMedium.copy(fontFamily = robotoFonts), + bodySmall = MaterialTheme.typography.bodySmall.copy(fontFamily = robotoFonts) + ) + + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + val insetsController = WindowCompat.getInsetsController(window, view) + insetsController.isAppearanceLightStatusBars = !useDarkTheme + } + } + + MaterialTheme( + colorScheme = predefinedColorScheme ?: colorScheme, + typography = typography, + content = content + ) +} diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AutoFill.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AutoFill.kt new file mode 100644 index 00000000..7aedae57 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AutoFill.kt @@ -0,0 +1,121 @@ +package com.meloda.app.fast.designsystem + +import android.os.Build +import android.view.autofill.AutofillManager +import androidx.annotation.RequiresApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.autofill.Autofill +import androidx.compose.ui.autofill.AutofillNode +import androidx.compose.ui.autofill.AutofillType +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalAutofill +import androidx.compose.ui.platform.LocalAutofillTree +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import kotlin.math.roundToInt + +fun Modifier.connectNode(handler: AutoFillHandler): Modifier { + return with(handler) { fillBounds() } +} + +fun Modifier.defaultFocusChangeAutoFill(handler: AutoFillHandler): Modifier { + return this.then( + Modifier.onFocusChanged { + if (it.isFocused) { + handler.request() + } else { + handler.cancel() + } + } + ) +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun autoFillRequestHandler( + autofillTypes: List = listOf(), + onFill: (String) -> Unit, +): AutoFillHandler { + val view = LocalView.current + val context = LocalContext.current + var isFillRecently = remember { false } + val autoFillNode = remember { + AutofillNode( + autofillTypes = autofillTypes, + onFill = { + isFillRecently = true + onFill(it) + } + ) + } + val autofill = LocalAutofill.current + LocalAutofillTree.current += autoFillNode + return remember { + @RequiresApi(Build.VERSION_CODES.O) + object : AutoFillHandler { + val autofillManager = context.getSystemService(AutofillManager::class.java) + override fun requestManual() { + autofillManager.requestAutofill( + view, + autoFillNode.id, + autoFillNode.boundingBox?.toAndroidRect() ?: error("BoundingBox is not provided yet") + ) + } + + override fun requestVerifyManual() { + if (isFillRecently) { + isFillRecently = false + requestManual() + } + } + + override val autoFill: Autofill? + get() = autofill + + override val autoFillNode: AutofillNode + get() = autoFillNode + + override fun request() { + autofill?.requestAutofillForNode(autofillNode = autoFillNode) + } + + override fun cancel() { + autofill?.cancelAutofillForNode(autofillNode = autoFillNode) + } + + override fun Modifier.fillBounds(): Modifier { + return this.then( + Modifier.onGloballyPositioned { + autoFillNode.boundingBox = it.boundsInWindow() + }) + } + } + } +} + +fun Rect.toAndroidRect(): android.graphics.Rect { + return android.graphics.Rect( + left.roundToInt(), + top.roundToInt(), + right.roundToInt(), + bottom.roundToInt() + ) +} + +@OptIn(ExperimentalComposeUiApi::class) +interface AutoFillHandler { + + val autoFill: Autofill? + val autoFillNode: AutofillNode + fun requestVerifyManual() + fun requestManual() + fun request() + fun cancel() + fun Modifier.fillBounds(): Modifier +} diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/ContentAlpha.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/ContentAlpha.kt new file mode 100644 index 00000000..ef32e974 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/ContentAlpha.kt @@ -0,0 +1,133 @@ +package com.meloda.app.fast.designsystem + +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import androidx.annotation.FloatRange +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.graphics.luminance + +/** + * Default alpha levels used by Material components. + * + * See [LocalContentAlpha]. + */ +object ContentAlpha { + /** + * A high level of content alpha, used to represent high emphasis text such as input text in a + * selected [TextField]. + */ + val high: Float + @Composable + get() = contentAlpha( + highContrastAlpha = HighContrastContentAlpha.high, + lowContrastAlpha = LowContrastContentAlpha.high + ) + + /** + * A medium level of content alpha, used to represent medium emphasis text such as + * placeholder text in a [TextField]. + */ + val medium: Float + @Composable + get() = contentAlpha( + highContrastAlpha = HighContrastContentAlpha.medium, + lowContrastAlpha = LowContrastContentAlpha.medium + ) + + /** + * A low level of content alpha used to represent disabled components, such as text in a + * disabled [Button]. + */ + val disabled: Float + @Composable + get() = contentAlpha( + highContrastAlpha = HighContrastContentAlpha.disabled, + lowContrastAlpha = LowContrastContentAlpha.disabled + ) + + /** + * This default implementation uses separate alpha levels depending on the luminance of the + * incoming color, and whether the theme is light or dark. This is to ensure correct contrast + * and accessibility on all surfaces. + * + * See [HighContrastContentAlpha] and [LowContrastContentAlpha] for what the levels are + * used for, and under what circumstances. + */ + @Composable + private fun contentAlpha( + @FloatRange(from = 0.0, to = 1.0) + highContrastAlpha: Float, + @FloatRange(from = 0.0, to = 1.0) + lowContrastAlpha: Float + ): Float { + val contentColor = LocalContentColor.current + return if (!isUsingDarkTheme()) { + if (contentColor.luminance() > 0.5) highContrastAlpha else lowContrastAlpha + } else { + if (contentColor.luminance() < 0.5) highContrastAlpha else lowContrastAlpha + } + } +} + +/** + * CompositionLocal containing the preferred content alpha for a given position in the hierarchy. + * This alpha is used for text and iconography ([Text] and [Icon]) to emphasize / de-emphasize + * different parts of a component. See the Material guide on + * [Text Legibility](https://material.io/design/color/text-legibility.html) for more information on + * alpha levels used by text and iconography. + * + * See [ContentAlpha] for the default levels used by most Material components. + * + * [MaterialTheme] sets this to [ContentAlpha.high] by default, as this is the default alpha for + * body text. + * + * @sample androidx.compose.material.samples.ContentAlphaSample + */ +val LocalContentAlpha = compositionLocalOf { 1f } + +/** + * Alpha levels for high luminance content in light theme, or low luminance content in dark theme. + * + * This content will typically be placed on colored surfaces, so it is important that the + * contrast here is higher to meet accessibility standards, and increase legibility. + * + * These levels are typically used for text / iconography in primary colored tabs / + * bottom navigation / etc. + */ +private object HighContrastContentAlpha { + const val high: Float = 1.00f + const val medium: Float = 0.74f + const val disabled: Float = 0.38f +} + +/** + * Alpha levels for low luminance content in light theme, or high luminance content in dark theme. + * + * This content will typically be placed on grayscale surfaces, so the contrast here can be lower + * without sacrificing accessibility and legibility. + * + * These levels are typically used for body text on the main surface (white in light theme, grey + * in dark theme) and text / iconography in surface colored tabs / bottom navigation / etc. + */ +private object LowContrastContentAlpha { + const val high: Float = 0.87f + const val medium: Float = 0.60f + const val disabled: Float = 0.38f +} diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/Extensions.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/Extensions.kt new file mode 100644 index 00000000..4e8fd568 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/Extensions.kt @@ -0,0 +1,112 @@ +package com.meloda.app.fast.designsystem + +import android.content.res.Configuration +import android.view.KeyEvent +import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionState +import com.google.accompanist.permissions.PermissionStatus +import com.meloda.app.fast.common.UiText +import com.meloda.app.fast.common.util.AndroidUtils +import com.meloda.app.fast.datastore.SettingsController +import com.meloda.app.fast.datastore.SettingsKeys + +@Composable +fun isUsingDarkTheme(): Boolean { + val nightThemeMode = SettingsController.getInt( + SettingsKeys.KEY_APPEARANCE_DARK_THEME, + SettingsKeys.DEFAULT_VALUE_APPEARANCE_DARK_THEME + ) + val appForceDarkMode = nightThemeMode == AppCompatDelegate.MODE_NIGHT_YES + val appBatterySaver = nightThemeMode == AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY + + val context = LocalContext.current + + val systemUiNightMode = context.resources.configuration.uiMode + + val isSystemBatterySaver = AndroidUtils.isBatterySaverOn(context) + val isSystemUsingDarkTheme = + systemUiNightMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + + return appForceDarkMode || (appBatterySaver && isSystemBatterySaver) || (!appBatterySaver && isSystemUsingDarkTheme && nightThemeMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) +} + +@Composable +fun UiText?.getString(): String? { + return when (this) { + is UiText.Resource -> { + stringResource(id = resId) + } + + is UiText.ResourceParams -> { + val processedArgs = args.map { any -> + when (any) { + is UiText -> any.getString().orEmpty() + else -> any.toString() + } + }.toTypedArray() + + stringResource(id = value, *processedArgs) + } + + is UiText.QuantityResource -> { + pluralStringResource(id = resId, count = quantity, quantity) + } + + is UiText.Simple -> text + + else -> null + } +} + +fun Modifier.handleTabKey( + action: () -> Boolean +): Modifier = this.onKeyEvent { event -> + if (event.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_TAB) { + action.invoke() + } else false +} + +fun Modifier.handleEnterKey( + action: () -> Boolean +): Modifier = this.onKeyEvent { event -> + if (event.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_ENTER) { + action.invoke() + } else false +} + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun CheckPermission( + showRationale: @Composable () -> Unit, + onDenied: @Composable () -> Unit, + permission: PermissionState, +) { + when (val status = permission.status) { + is PermissionStatus.Denied -> { + if (status.shouldShowRationale) { + showRationale() + } else { + onDenied() + } + } + + is PermissionStatus.Granted -> Unit + } +} + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun RequestPermission( + permission: PermissionState +) { + LaunchedEffect(Unit) { permission.launchPermissionRequest() } +} + diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/ImmutableList.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/ImmutableList.kt new file mode 100644 index 00000000..7ea0e6fb --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/ImmutableList.kt @@ -0,0 +1,59 @@ +package com.meloda.app.fast.designsystem + +import androidx.compose.runtime.Immutable + +@Immutable +class ImmutableList(val values: List) : Iterable { + + constructor(size: Int, init: (index: Int) -> T) : this(MutableList(size, init)) + + operator fun get(index: Int): T? { + values.singleOrNull() + return values[index] + } + + inline fun forEach(action: (T) -> Unit) { + for (element in values) action(element) + } + + inline fun map(transform: (T) -> R): ImmutableList { + return values.map(transform).toImmutableList() + } + + inline fun mapIndexed(transform: (index: Int, T) -> R): ImmutableList { + return values.mapIndexed(transform).toImmutableList() + } + + fun singleOrNull(): T? { + return if (values.size == 1) this[0] else null + } + + fun isEmpty(): Boolean = values.isEmpty() + + fun isNotEmpty(): Boolean = !isEmpty() + + inline fun singleOrNull(predicate: (T) -> Boolean): T? { + var single: T? = null + var found = false + for (element in this) { + if (predicate(element)) { + if (found) return null + single = element + found = true + } + } + if (!found) return null + return single + } + + companion object { + fun copyOf(collection: Collection): ImmutableList = + ImmutableList(collection.toList()) + + fun List.toImmutableList(): ImmutableList = ImmutableList(this) + + fun empty(): ImmutableList = ImmutableList(emptyList()) + } + + override fun iterator(): Iterator = values.listIterator() +} diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/LocalContentAlpha.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/LocalContentAlpha.kt new file mode 100644 index 00000000..45671625 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/LocalContentAlpha.kt @@ -0,0 +1,20 @@ +package com.meloda.app.fast.designsystem + +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.graphics.Color + +@Composable +fun LocalContentAlpha( + defaultColor: Color = MaterialTheme.colorScheme.onBackground, + alpha: Float, + content: @Composable () -> Unit +) { + CompositionLocalProvider( + LocalContentColor provides defaultColor.copy(alpha = alpha) + ) { + content() + } +} diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/MaterialDialog.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/MaterialDialog.kt new file mode 100644 index 00000000..853b3545 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/MaterialDialog.kt @@ -0,0 +1,346 @@ +package com.meloda.app.fast.designsystem + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.meloda.app.fast.common.UiText +import com.meloda.app.fast.designsystem.ImmutableList.Companion.toImmutableList + +// TODO: 08.04.2023, Danil Nikolaev: review +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MaterialDialog( + onDismissAction: (() -> Unit), + title: UiText? = null, + text: UiText? = null, + confirmText: UiText? = null, + confirmAction: (() -> Unit)? = null, + cancelText: UiText? = null, + cancelAction: (() -> Unit)? = null, + neutralText: UiText? = null, + neutralAction: (() -> Unit)? = null, + itemsSelectionType: ItemsSelectionType = ItemsSelectionType.None, + preSelectedItems: ImmutableList = ImmutableList.empty(), + items: ImmutableList = ImmutableList.empty(), + onItemClick: ((index: Int) -> Unit)? = null, + buttonsInvokeDismiss: Boolean = true, + customContent: (@Composable ColumnScope.() -> Unit)? = null, +) { + var isVisible by remember { + mutableStateOf(true) + } + val onDismissRequest = { + onDismissAction.invoke() + isVisible = false + } + + val stringTitles = items.map { it.getString().orEmpty() } + + var alertItems by remember { + mutableStateOf( + stringTitles.mapIndexed { index, title -> + DialogItem( + title, + preSelectedItems.contains(index) + ) + } + ) + } + + AppTheme { + if (isVisible) { +// AlertAnimation(visible = isVisible) { + BasicAlertDialog( + onDismissRequest = onDismissRequest + ) { + val scrollState = rememberScrollState() + val canScrollBackward by remember { derivedStateOf { scrollState.value > 0 } } + val canScrollForward by remember { derivedStateOf { scrollState.value < scrollState.maxValue } } + + Surface( + modifier = Modifier.fillMaxWidth(), + color = AlertDialogDefaults.containerColor, + shape = AlertDialogDefaults.shape, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + Column(modifier = Modifier.padding(bottom = 10.dp)) { + val stringTitle = title?.getString() + if (stringTitle != null) { + Spacer(modifier = Modifier.height(20.dp)) + } + + Row { + stringTitle?.let { title -> + Spacer(modifier = Modifier.width(24.dp)) + Text( + modifier = Modifier.weight(1f), + text = title, + style = MaterialTheme.typography.headlineSmall + ) + Spacer(modifier = Modifier.width(20.dp)) + } + } + + if (canScrollBackward) { + HorizontalDivider(modifier = Modifier.fillMaxWidth()) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f, fill = false) + .verticalScroll(scrollState) + ) { + Spacer(modifier = Modifier.height(8.dp)) + + val stringMessage = text?.getString() + if (stringMessage != null && stringTitle == null) { + Spacer(modifier = Modifier.height(20.dp)) + } + + Row { + stringMessage?.let { message -> + Spacer(modifier = Modifier.width(24.dp)) + Text( + modifier = Modifier.weight(1f), + text = message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.width(20.dp)) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + if (alertItems.isNotEmpty()) { + Spacer(modifier = Modifier.height(4.dp)) + AlertItems( + selectionType = itemsSelectionType, + items = alertItems, + onItemClick = { index -> + onItemClick?.invoke(index) + + if (itemsSelectionType == ItemsSelectionType.None) { + onDismissRequest.invoke() + } else { + val newItems = + alertItems.mapIndexed { itemIndex, item -> + item.copy(isSelected = itemIndex == index) + } + + alertItems = newItems + } + }, + onItemCheckedChanged = { index -> + val newItems = alertItems.toMutableList() + val oldItem = newItems[index] + newItems[index] = + oldItem.copy(isSelected = !oldItem.isSelected) + + alertItems = newItems.toImmutableList() + } + ) + Spacer(modifier = Modifier.height(10.dp)) + } else { + customContent?.let { content -> + Spacer(modifier = Modifier.height(4.dp)) + content.invoke(this) + Spacer(modifier = Modifier.height(10.dp)) + } + } + } + + if (canScrollForward) { + HorizontalDivider(modifier = Modifier.fillMaxWidth()) + } + + Row { + Spacer(modifier = Modifier.width(20.dp)) + neutralText?.getString()?.let { text -> + TextButton( + onClick = { + if (buttonsInvokeDismiss) { + onDismissRequest.invoke() + } else { + isVisible = false + } + neutralAction?.invoke() + } + ) { + Text(text = text) + } + } + + Spacer(modifier = Modifier.weight(1f)) + + cancelText?.getString()?.let { text -> + TextButton( + onClick = { + if (buttonsInvokeDismiss) { + onDismissRequest.invoke() + } else { + isVisible = false + } + cancelAction?.invoke() + } + ) { + Text(text = text) + } + } + + Spacer(modifier = Modifier.width(2.dp)) + + confirmText?.getString()?.let { text -> + TextButton( + onClick = { + if (buttonsInvokeDismiss) { + onDismissRequest.invoke() + } else { + isVisible = false + } + confirmAction?.invoke() + } + ) { + Text(text = text) + } + } + + Spacer(modifier = Modifier.width(20.dp)) + } + } + } + } + } + } +} + +@Composable +fun AlertAnimation( + visible: Boolean, + content: @Composable AnimatedVisibilityScope.() -> Unit +) { + AnimatedVisibility( + visible = visible, + enter = fadeIn(animationSpec = tween(400)) + + scaleIn(animationSpec = tween(400)), + exit = fadeOut(animationSpec = tween(150)), + content = content + ) +} + +@Preview +@Composable +fun AlertItemsPreview() { + AppTheme { + AlertItems( + selectionType = ItemsSelectionType.None, + items = ImmutableList(5) { index -> + DialogItem( + title = "Item #${index + 1}", + isSelected = index % 2 == 0 + ) + }, + onItemClick = {} + ) + } +} + +@Composable +private fun AlertItems( + selectionType: ItemsSelectionType, + items: ImmutableList, + onItemClick: ((index: Int) -> Unit)? = null, + onItemCheckedChanged: ((index: Int) -> Unit)? = null +) { + items.forEachIndexed { index, item -> + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .clickable { + if (selectionType == ItemsSelectionType.Multi) { + onItemCheckedChanged?.invoke(index) + } else { + onItemClick?.invoke(index) + } + }, + verticalAlignment = Alignment.CenterVertically + ) { + // TODO: 29/12/2023, Danil Nikolaev: check onClick & onCheckedChange actions + when (selectionType) { + ItemsSelectionType.Multi -> { + Spacer(modifier = Modifier.width(10.dp)) + Checkbox( + checked = item.isSelected, + onCheckedChange = {} + ) + } + + ItemsSelectionType.Single -> { + Spacer(modifier = Modifier.width(10.dp)) + RadioButton( + selected = item.isSelected, + onClick = {} + ) + } + + ItemsSelectionType.None -> { + Spacer(modifier = Modifier.width(26.dp)) + } + } + Spacer(modifier = Modifier.width(4.dp)) + Text( + modifier = Modifier.weight(1f), + text = item.title, + style = MaterialTheme.typography.bodyLarge + ) + Spacer(modifier = Modifier.width(20.dp)) + } + } +} + +data class DialogItem( + val title: String, + val isSelected: Boolean +) + +sealed interface ItemsSelectionType { + data object Single : ItemsSelectionType + data object Multi : ItemsSelectionType + data object None : ItemsSelectionType +} diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/TabItem.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/TabItem.kt new file mode 100644 index 00000000..5d32f63c --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/TabItem.kt @@ -0,0 +1,7 @@ +package com.meloda.app.fast.designsystem + +data class TabItem( + val titleResId: Int?, + val unselectedIconResId: Int?, + val selectedIconResId: Int? +) diff --git a/app/src/main/kotlin/com/meloda/fast/ui/widgets/TextFieldErrorText.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/TextFieldErrorText.kt similarity index 83% rename from app/src/main/kotlin/com/meloda/fast/ui/widgets/TextFieldErrorText.kt rename to core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/TextFieldErrorText.kt index d130759f..212d8a13 100644 --- a/app/src/main/kotlin/com/meloda/fast/ui/widgets/TextFieldErrorText.kt +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/TextFieldErrorText.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.ui.widgets +package com.meloda.app.fast.designsystem import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -7,6 +7,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -14,7 +15,7 @@ import androidx.compose.ui.unit.dp @Composable fun TextFieldErrorText( modifier: Modifier = Modifier, - text: String = "Field must not be empty", + text: String = stringResource(id = R.string.error_empty_field), withSpacer: Boolean = true ) { Row { diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/colorschemes/ClassicColorScheme.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/colorschemes/ClassicColorScheme.kt new file mode 100644 index 00000000..0c26289c --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/colorschemes/ClassicColorScheme.kt @@ -0,0 +1,155 @@ +package com.meloda.app.fast.designsystem.colorschemes + +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +object ClassicColorScheme { + private val primaryLight = Color(0xFF405F90) + private val onPrimaryLight = Color(0xFFFFFFFF) + private val primaryContainerLight = Color(0xFFD6E3FF) + private val onPrimaryContainerLight = Color(0xFF001B3D) + private val secondaryLight = Color(0xFF555F71) + private val onSecondaryLight = Color(0xFFFFFFFF) + private val secondaryContainerLight = Color(0xFFDAE2F9) + private val onSecondaryContainerLight = Color(0xFF121C2B) + private val tertiaryLight = Color(0xFF6F5575) + private val onTertiaryLight = Color(0xFFFFFFFF) + private val tertiaryContainerLight = Color(0xFFF9D8FD) + private val onTertiaryContainerLight = Color(0xFF28132F) + private val errorLight = Color(0xFFBA1A1A) + private val onErrorLight = Color(0xFFFFFFFF) + private val errorContainerLight = Color(0xFFFFDAD6) + private val onErrorContainerLight = Color(0xFF410002) + private val backgroundLight = Color(0xFFF9F9FF) + private val onBackgroundLight = Color(0xFF191C20) + private val surfaceLight = Color(0xFFF9F9FF) + private val onSurfaceLight = Color(0xFF191C20) + private val surfaceVariantLight = Color(0xFFE0E2EC) + private val onSurfaceVariantLight = Color(0xFF44474E) + private val outlineLight = Color(0xFF74777F) + private val outlineVariantLight = Color(0xFFC4C6CF) + private val scrimLight = Color(0xFF000000) + private val inverseSurfaceLight = Color(0xFF2E3036) + private val inverseOnSurfaceLight = Color(0xFFF0F0F7) + private val inversePrimaryLight = Color(0xFFA9C7FF) + private val surfaceDimLight = Color(0xFFD9D9E0) + private val surfaceBrightLight = Color(0xFFF9F9FF) + private val surfaceContainerLowestLight = Color(0xFFFFFFFF) + private val surfaceContainerLowLight = Color(0xFFF3F3FA) + private val surfaceContainerLight = Color(0xFFEDEDF4) + private val surfaceContainerHighLight = Color(0xFFE7E8EE) + private val surfaceContainerHighestLight = Color(0xFFE2E2E9) + + private val primaryDark = Color(0xFFA9C7FF) + private val onPrimaryDark = Color(0xFF08305F) + private val primaryContainerDark = Color(0xFF274777) + private val onPrimaryContainerDark = Color(0xFFD6E3FF) + private val secondaryDark = Color(0xFFBDC7DC) + private val onSecondaryDark = Color(0xFF283141) + private val secondaryContainerDark = Color(0xFF3E4758) + private val onSecondaryContainerDark = Color(0xFFDAE2F9) + private val tertiaryDark = Color(0xFFDCBCE1) + private val onTertiaryDark = Color(0xFF3F2845) + private val tertiaryContainerDark = Color(0xFF563E5C) + private val onTertiaryContainerDark = Color(0xFFF9D8FD) + private val errorDark = Color(0xFFFFB4AB) + private val onErrorDark = Color(0xFF690005) + private val errorContainerDark = Color(0xFF93000A) + private val onErrorContainerDark = Color(0xFFFFDAD6) + private val backgroundDark = Color(0xFF111318) + private val onBackgroundDark = Color(0xFFE2E2E9) + private val surfaceDark = Color(0xFF111318) + private val onSurfaceDark = Color(0xFFE2E2E9) + private val surfaceVariantDark = Color(0xFF44474E) + private val onSurfaceVariantDark = Color(0xFFC4C6CF) + private val outlineDark = Color(0xFF8E9099) + private val outlineVariantDark = Color(0xFF44474E) + private val scrimDark = Color(0xFF000000) + private val inverseSurfaceDark = Color(0xFFE2E2E9) + private val inverseOnSurfaceDark = Color(0xFF2E3036) + private val inversePrimaryDark = Color(0xFF405F90) + private val surfaceDimDark = Color(0xFF111318) + private val surfaceBrightDark = Color(0xFF37393E) + private val surfaceContainerLowestDark = Color(0xFF0C0E13) + private val surfaceContainerLowDark = Color(0xFF191C20) + private val surfaceContainerDark = Color(0xFF1D2024) + private val surfaceContainerHighDark = Color(0xFF282A2F) + private val surfaceContainerHighestDark = Color(0xFF33353A) + + val darkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, + ) + + val lightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, + ) +} diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/BlurrableTopAppBar.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/BlurrableTopAppBar.kt new file mode 100644 index 00000000..1684249b --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/BlurrableTopAppBar.kt @@ -0,0 +1,81 @@ +package com.meloda.app.fast.designsystem.components + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.meloda.app.fast.designsystem.LocalTheme +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeChild +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials + +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalHazeMaterialsApi::class +) +@Composable +fun BlurrableTopAppBar( + modifier: Modifier = Modifier, + title: String, + listState: LazyListState?, + hazeState: HazeState = remember { HazeState() } +) { + val currentTheme = LocalTheme.current + + val toolbarColorAlpha by animateFloatAsState( + targetValue = if (listState == null || !listState.canScrollBackward) 1f else 0f, + label = "toolbarColorAlpha", + animationSpec = tween(durationMillis = 50) + ) + + val toolbarContainerColor by animateColorAsState( + targetValue = + if (currentTheme.usingBlur || listState != null && !listState.canScrollBackward) + MaterialTheme.colorScheme.surface + else + MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp), + label = "toolbarColorAlpha", + animationSpec = tween(durationMillis = 50) + ) + + TopAppBar( + title = { + Text( + text = title, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = toolbarContainerColor.copy( + alpha = if (currentTheme.usingBlur) toolbarColorAlpha else 1f + ) + ), + modifier = modifier + .then( + if (currentTheme.usingBlur) { + Modifier.hazeChild( + state = hazeState, + style = HazeMaterials.thick() + ) + } else { + Modifier + } + ) + .fillMaxWidth(), + ) +} diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/FullScreenLoader.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/FullScreenLoader.kt new file mode 100644 index 00000000..9c487b95 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/FullScreenLoader.kt @@ -0,0 +1,21 @@ +package com.meloda.app.fast.designsystem.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun FullScreenLoader(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } +} diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/NoItemsView.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/NoItemsView.kt new file mode 100644 index 00000000..d60de134 --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/components/NoItemsView.kt @@ -0,0 +1,27 @@ +package com.meloda.app.fast.designsystem.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.meloda.app.fast.designsystem.R + +@Composable +fun NoItemsView( + modifier: Modifier = Modifier, + customText: String? = null +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = customText ?: stringResource(id = R.string.no_items), + style = MaterialTheme.typography.titleLarge + ) + } +} diff --git a/core/designsystem/src/main/res/drawable/baseline_account_circle_24.xml b/core/designsystem/src/main/res/drawable/baseline_account_circle_24.xml new file mode 100644 index 00000000..1e24cf39 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/baseline_account_circle_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/core/designsystem/src/main/res/drawable/baseline_chat_24.xml b/core/designsystem/src/main/res/drawable/baseline_chat_24.xml new file mode 100644 index 00000000..7f6fda16 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/baseline_chat_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/core/designsystem/src/main/res/drawable/baseline_people_alt_24.xml b/core/designsystem/src/main/res/drawable/baseline_people_alt_24.xml new file mode 100644 index 00000000..90c82148 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/baseline_people_alt_24.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_account_circle_cut.xml b/core/designsystem/src/main/res/drawable/ic_account_circle_cut.xml similarity index 100% rename from app/src/main/res/drawable/ic_account_circle_cut.xml rename to core/designsystem/src/main/res/drawable/ic_account_circle_cut.xml diff --git a/app/src/main/res/drawable/ic_arrow_end.xml b/core/designsystem/src/main/res/drawable/ic_arrow_end.xml similarity index 100% rename from app/src/main/res/drawable/ic_arrow_end.xml rename to core/designsystem/src/main/res/drawable/ic_arrow_end.xml diff --git a/app/src/main/res/drawable/ic_attachment_audio.xml b/core/designsystem/src/main/res/drawable/ic_attachment_audio.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_audio.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_audio.xml diff --git a/app/src/main/res/drawable/ic_attachment_call.xml b/core/designsystem/src/main/res/drawable/ic_attachment_call.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_call.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_call.xml diff --git a/app/src/main/res/drawable/ic_attachment_file.xml b/core/designsystem/src/main/res/drawable/ic_attachment_file.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_file.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_file.xml diff --git a/app/src/main/res/drawable/ic_attachment_forwarded_message.xml b/core/designsystem/src/main/res/drawable/ic_attachment_forwarded_message.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_forwarded_message.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_forwarded_message.xml diff --git a/app/src/main/res/drawable/ic_attachment_forwarded_messages.xml b/core/designsystem/src/main/res/drawable/ic_attachment_forwarded_messages.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_forwarded_messages.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_forwarded_messages.xml diff --git a/app/src/main/res/drawable/ic_attachment_gift.xml b/core/designsystem/src/main/res/drawable/ic_attachment_gift.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_gift.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_gift.xml diff --git a/app/src/main/res/drawable/ic_attachment_graffiti.xml b/core/designsystem/src/main/res/drawable/ic_attachment_graffiti.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_graffiti.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_graffiti.xml diff --git a/app/src/main/res/drawable/ic_attachment_group_call.xml b/core/designsystem/src/main/res/drawable/ic_attachment_group_call.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_group_call.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_group_call.xml diff --git a/app/src/main/res/drawable/ic_attachment_link.xml b/core/designsystem/src/main/res/drawable/ic_attachment_link.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_link.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_link.xml diff --git a/app/src/main/res/drawable/ic_attachment_mini_app.xml b/core/designsystem/src/main/res/drawable/ic_attachment_mini_app.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_mini_app.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_mini_app.xml diff --git a/app/src/main/res/drawable/ic_attachment_photo.xml b/core/designsystem/src/main/res/drawable/ic_attachment_photo.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_photo.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_photo.xml diff --git a/app/src/main/res/drawable/ic_attachment_poll.xml b/core/designsystem/src/main/res/drawable/ic_attachment_poll.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_poll.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_poll.xml diff --git a/app/src/main/res/drawable/ic_attachment_sticker.xml b/core/designsystem/src/main/res/drawable/ic_attachment_sticker.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_sticker.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_sticker.xml diff --git a/app/src/main/res/drawable/ic_attachment_story.xml b/core/designsystem/src/main/res/drawable/ic_attachment_story.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_story.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_story.xml diff --git a/app/src/main/res/drawable/ic_attachment_video.xml b/core/designsystem/src/main/res/drawable/ic_attachment_video.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_video.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_video.xml diff --git a/app/src/main/res/drawable/ic_attachment_voice.xml b/core/designsystem/src/main/res/drawable/ic_attachment_voice.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_voice.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_voice.xml diff --git a/app/src/main/res/drawable/ic_attachment_wall.xml b/core/designsystem/src/main/res/drawable/ic_attachment_wall.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_wall.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_wall.xml diff --git a/app/src/main/res/drawable/ic_attachment_wall_reply.xml b/core/designsystem/src/main/res/drawable/ic_attachment_wall_reply.xml similarity index 100% rename from app/src/main/res/drawable/ic_attachment_wall_reply.xml rename to core/designsystem/src/main/res/drawable/ic_attachment_wall_reply.xml diff --git a/app/src/main/res/drawable/ic_baseline_attach_file_24.xml b/core/designsystem/src/main/res/drawable/ic_baseline_attach_file_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_baseline_attach_file_24.xml rename to core/designsystem/src/main/res/drawable/ic_baseline_attach_file_24.xml diff --git a/app/src/main/res/drawable/ic_baseline_create_24.xml b/core/designsystem/src/main/res/drawable/ic_baseline_create_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_baseline_create_24.xml rename to core/designsystem/src/main/res/drawable/ic_baseline_create_24.xml diff --git a/app/src/main/res/drawable/ic_fast_logo.xml b/core/designsystem/src/main/res/drawable/ic_fast_logo.xml similarity index 100% rename from app/src/main/res/drawable/ic_fast_logo.xml rename to core/designsystem/src/main/res/drawable/ic_fast_logo.xml diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/core/designsystem/src/main/res/drawable/ic_launcher_foreground.xml similarity index 100% rename from app/src/main/res/drawable/ic_launcher_foreground.xml rename to core/designsystem/src/main/res/drawable/ic_launcher_foreground.xml diff --git a/app/src/main/res/drawable/ic_logo_big.xml b/core/designsystem/src/main/res/drawable/ic_logo_big.xml similarity index 100% rename from app/src/main/res/drawable/ic_logo_big.xml rename to core/designsystem/src/main/res/drawable/ic_logo_big.xml diff --git a/app/src/main/res/drawable/ic_map_marker.xml b/core/designsystem/src/main/res/drawable/ic_map_marker.xml similarity index 100% rename from app/src/main/res/drawable/ic_map_marker.xml rename to core/designsystem/src/main/res/drawable/ic_map_marker.xml diff --git a/app/src/main/res/drawable/ic_outline_emoji_emotions_24.xml b/core/designsystem/src/main/res/drawable/ic_outline_emoji_emotions_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_outline_emoji_emotions_24.xml rename to core/designsystem/src/main/res/drawable/ic_outline_emoji_emotions_24.xml diff --git a/app/src/main/res/drawable/ic_round_add_circle_outline_24.xml b/core/designsystem/src/main/res/drawable/ic_round_add_circle_outline_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_round_add_circle_outline_24.xml rename to core/designsystem/src/main/res/drawable/ic_round_add_circle_outline_24.xml diff --git a/app/src/main/res/drawable/ic_round_arrow_back_24.xml b/core/designsystem/src/main/res/drawable/ic_round_arrow_back_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_round_arrow_back_24.xml rename to core/designsystem/src/main/res/drawable/ic_round_arrow_back_24.xml diff --git a/app/src/main/res/drawable/ic_round_bookmark_border_24.xml b/core/designsystem/src/main/res/drawable/ic_round_bookmark_border_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_round_bookmark_border_24.xml rename to core/designsystem/src/main/res/drawable/ic_round_bookmark_border_24.xml diff --git a/app/src/main/res/drawable/ic_round_close_24.xml b/core/designsystem/src/main/res/drawable/ic_round_close_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_round_close_24.xml rename to core/designsystem/src/main/res/drawable/ic_round_close_24.xml diff --git a/app/src/main/res/drawable/ic_round_done_24.xml b/core/designsystem/src/main/res/drawable/ic_round_done_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_round_done_24.xml rename to core/designsystem/src/main/res/drawable/ic_round_done_24.xml diff --git a/app/src/main/res/drawable/ic_round_mic_none_24.xml b/core/designsystem/src/main/res/drawable/ic_round_mic_none_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_round_mic_none_24.xml rename to core/designsystem/src/main/res/drawable/ic_round_mic_none_24.xml diff --git a/app/src/main/res/drawable/ic_round_person_24.xml b/core/designsystem/src/main/res/drawable/ic_round_person_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_round_person_24.xml rename to core/designsystem/src/main/res/drawable/ic_round_person_24.xml diff --git a/app/src/main/res/drawable/ic_round_push_pin_24.xml b/core/designsystem/src/main/res/drawable/ic_round_push_pin_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_round_push_pin_24.xml rename to core/designsystem/src/main/res/drawable/ic_round_push_pin_24.xml diff --git a/core/designsystem/src/main/res/drawable/outline_account_circle_24.xml b/core/designsystem/src/main/res/drawable/outline_account_circle_24.xml new file mode 100644 index 00000000..c85da5ee --- /dev/null +++ b/core/designsystem/src/main/res/drawable/outline_account_circle_24.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/outline_chat_24.xml b/core/designsystem/src/main/res/drawable/outline_chat_24.xml new file mode 100644 index 00000000..7ce81fa5 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/outline_chat_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/core/designsystem/src/main/res/drawable/outline_people_alt_24.xml b/core/designsystem/src/main/res/drawable/outline_people_alt_24.xml new file mode 100644 index 00000000..f3e073ee --- /dev/null +++ b/core/designsystem/src/main/res/drawable/outline_people_alt_24.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/round_more_vert_24.xml b/core/designsystem/src/main/res/drawable/pin_off_outline_24.xml similarity index 52% rename from app/src/main/res/drawable/round_more_vert_24.xml rename to core/designsystem/src/main/res/drawable/pin_off_outline_24.xml index b29b2aef..872faead 100644 --- a/app/src/main/res/drawable/round_more_vert_24.xml +++ b/core/designsystem/src/main/res/drawable/pin_off_outline_24.xml @@ -1,3 +1,4 @@ + + android:pathData="M8,6.2V4H7V2H17V4H16V12L18,14V16H17.8L14,12.2V4H10V8.2L8,6.2M20,20.7L18.7,22L12.8,16.1V22H11.2V16H6V14L8,12V11.3L2,5.3L3.3,4L20,20.7M8.8,14H10.6L9.7,13.1L8.8,14Z" /> diff --git a/app/src/main/res/drawable/ic_trash_can_outline_24.xml b/core/designsystem/src/main/res/drawable/pin_outline_24.xml similarity index 65% rename from app/src/main/res/drawable/ic_trash_can_outline_24.xml rename to core/designsystem/src/main/res/drawable/pin_outline_24.xml index 05862a21..fb9b7e19 100644 --- a/app/src/main/res/drawable/ic_trash_can_outline_24.xml +++ b/core/designsystem/src/main/res/drawable/pin_outline_24.xml @@ -6,5 +6,5 @@ android:viewportHeight="24"> - \ No newline at end of file + android:pathData="M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12M8.8,14L10,12.8V4H14V12.8L15.2,14H8.8Z" /> + diff --git a/core/designsystem/src/main/res/drawable/round_attach_file_24.xml b/core/designsystem/src/main/res/drawable/round_attach_file_24.xml new file mode 100644 index 00000000..9e4d42bc --- /dev/null +++ b/core/designsystem/src/main/res/drawable/round_attach_file_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/round_cake_24.xml b/core/designsystem/src/main/res/drawable/round_cake_24.xml similarity index 100% rename from app/src/main/res/drawable/round_cake_24.xml rename to core/designsystem/src/main/res/drawable/round_cake_24.xml diff --git a/core/designsystem/src/main/res/drawable/round_delete_outline_24.xml b/core/designsystem/src/main/res/drawable/round_delete_outline_24.xml new file mode 100644 index 00000000..8ab6f1f0 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/round_delete_outline_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_round_done_all_24.xml b/core/designsystem/src/main/res/drawable/round_done_all_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_round_done_all_24.xml rename to core/designsystem/src/main/res/drawable/round_done_all_24.xml diff --git a/app/src/main/res/drawable/round_file_download_24.xml b/core/designsystem/src/main/res/drawable/round_file_download_24.xml similarity index 100% rename from app/src/main/res/drawable/round_file_download_24.xml rename to core/designsystem/src/main/res/drawable/round_file_download_24.xml diff --git a/app/src/main/res/drawable/round_install_mobile_24.xml b/core/designsystem/src/main/res/drawable/round_install_mobile_24.xml similarity index 100% rename from app/src/main/res/drawable/round_install_mobile_24.xml rename to core/designsystem/src/main/res/drawable/round_install_mobile_24.xml diff --git a/app/src/main/res/drawable/round_qr_code_24.xml b/core/designsystem/src/main/res/drawable/round_qr_code_24.xml similarity index 100% rename from app/src/main/res/drawable/round_qr_code_24.xml rename to core/designsystem/src/main/res/drawable/round_qr_code_24.xml diff --git a/app/src/main/res/drawable/round_restart_alt_24.xml b/core/designsystem/src/main/res/drawable/round_restart_alt_24.xml similarity index 100% rename from app/src/main/res/drawable/round_restart_alt_24.xml rename to core/designsystem/src/main/res/drawable/round_restart_alt_24.xml diff --git a/core/designsystem/src/main/res/drawable/round_send_24.xml b/core/designsystem/src/main/res/drawable/round_send_24.xml new file mode 100644 index 00000000..94dcd656 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/round_send_24.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/round_sms_24.xml b/core/designsystem/src/main/res/drawable/round_sms_24.xml similarity index 100% rename from app/src/main/res/drawable/round_sms_24.xml rename to core/designsystem/src/main/res/drawable/round_sms_24.xml diff --git a/app/src/main/res/drawable/round_visibility_24.xml b/core/designsystem/src/main/res/drawable/round_visibility_24.xml similarity index 100% rename from app/src/main/res/drawable/round_visibility_24.xml rename to core/designsystem/src/main/res/drawable/round_visibility_24.xml diff --git a/app/src/main/res/drawable/round_visibility_off_24.xml b/core/designsystem/src/main/res/drawable/round_visibility_off_24.xml similarity index 100% rename from app/src/main/res/drawable/round_visibility_off_24.xml rename to core/designsystem/src/main/res/drawable/round_visibility_off_24.xml diff --git a/app/src/main/res/drawable/round_vpn_key_24.xml b/core/designsystem/src/main/res/drawable/round_vpn_key_24.xml similarity index 100% rename from app/src/main/res/drawable/round_vpn_key_24.xml rename to core/designsystem/src/main/res/drawable/round_vpn_key_24.xml diff --git a/app/src/main/res/drawable/test_captcha.webp b/core/designsystem/src/main/res/drawable/test_captcha.webp similarity index 100% rename from app/src/main/res/drawable/test_captcha.webp rename to core/designsystem/src/main/res/drawable/test_captcha.webp diff --git a/app/src/main/res/font/google_sans_bold.ttf b/core/designsystem/src/main/res/font/google_sans_bold.ttf similarity index 100% rename from app/src/main/res/font/google_sans_bold.ttf rename to core/designsystem/src/main/res/font/google_sans_bold.ttf diff --git a/app/src/main/res/font/google_sans_bold_italic.ttf b/core/designsystem/src/main/res/font/google_sans_bold_italic.ttf similarity index 100% rename from app/src/main/res/font/google_sans_bold_italic.ttf rename to core/designsystem/src/main/res/font/google_sans_bold_italic.ttf diff --git a/app/src/main/res/font/google_sans_italic.ttf b/core/designsystem/src/main/res/font/google_sans_italic.ttf similarity index 100% rename from app/src/main/res/font/google_sans_italic.ttf rename to core/designsystem/src/main/res/font/google_sans_italic.ttf diff --git a/app/src/main/res/font/google_sans_medium.ttf b/core/designsystem/src/main/res/font/google_sans_medium.ttf similarity index 100% rename from app/src/main/res/font/google_sans_medium.ttf rename to core/designsystem/src/main/res/font/google_sans_medium.ttf diff --git a/app/src/main/res/font/google_sans_medium_italic.ttf b/core/designsystem/src/main/res/font/google_sans_medium_italic.ttf similarity index 100% rename from app/src/main/res/font/google_sans_medium_italic.ttf rename to core/designsystem/src/main/res/font/google_sans_medium_italic.ttf diff --git a/app/src/main/res/font/google_sans_regular.ttf b/core/designsystem/src/main/res/font/google_sans_regular.ttf similarity index 100% rename from app/src/main/res/font/google_sans_regular.ttf rename to core/designsystem/src/main/res/font/google_sans_regular.ttf diff --git a/app/src/main/res/font/roboto_black.ttf b/core/designsystem/src/main/res/font/roboto_black.ttf similarity index 100% rename from app/src/main/res/font/roboto_black.ttf rename to core/designsystem/src/main/res/font/roboto_black.ttf diff --git a/app/src/main/res/font/roboto_black_italic.ttf b/core/designsystem/src/main/res/font/roboto_black_italic.ttf similarity index 100% rename from app/src/main/res/font/roboto_black_italic.ttf rename to core/designsystem/src/main/res/font/roboto_black_italic.ttf diff --git a/app/src/main/res/font/roboto_bold.ttf b/core/designsystem/src/main/res/font/roboto_bold.ttf similarity index 100% rename from app/src/main/res/font/roboto_bold.ttf rename to core/designsystem/src/main/res/font/roboto_bold.ttf diff --git a/app/src/main/res/font/roboto_bold_italic.ttf b/core/designsystem/src/main/res/font/roboto_bold_italic.ttf similarity index 100% rename from app/src/main/res/font/roboto_bold_italic.ttf rename to core/designsystem/src/main/res/font/roboto_bold_italic.ttf diff --git a/app/src/main/res/font/roboto_italic.ttf b/core/designsystem/src/main/res/font/roboto_italic.ttf similarity index 100% rename from app/src/main/res/font/roboto_italic.ttf rename to core/designsystem/src/main/res/font/roboto_italic.ttf diff --git a/app/src/main/res/font/roboto_light.ttf b/core/designsystem/src/main/res/font/roboto_light.ttf similarity index 100% rename from app/src/main/res/font/roboto_light.ttf rename to core/designsystem/src/main/res/font/roboto_light.ttf diff --git a/app/src/main/res/font/roboto_light_italic.ttf b/core/designsystem/src/main/res/font/roboto_light_italic.ttf similarity index 100% rename from app/src/main/res/font/roboto_light_italic.ttf rename to core/designsystem/src/main/res/font/roboto_light_italic.ttf diff --git a/app/src/main/res/font/roboto_medium.ttf b/core/designsystem/src/main/res/font/roboto_medium.ttf similarity index 100% rename from app/src/main/res/font/roboto_medium.ttf rename to core/designsystem/src/main/res/font/roboto_medium.ttf diff --git a/app/src/main/res/font/roboto_medium_italic.ttf b/core/designsystem/src/main/res/font/roboto_medium_italic.ttf similarity index 100% rename from app/src/main/res/font/roboto_medium_italic.ttf rename to core/designsystem/src/main/res/font/roboto_medium_italic.ttf diff --git a/app/src/main/res/font/roboto_regular.ttf b/core/designsystem/src/main/res/font/roboto_regular.ttf similarity index 100% rename from app/src/main/res/font/roboto_regular.ttf rename to core/designsystem/src/main/res/font/roboto_regular.ttf diff --git a/app/src/main/res/font/roboto_thin.ttf b/core/designsystem/src/main/res/font/roboto_thin.ttf similarity index 100% rename from app/src/main/res/font/roboto_thin.ttf rename to core/designsystem/src/main/res/font/roboto_thin.ttf diff --git a/app/src/main/res/font/roboto_thin_italic.ttf b/core/designsystem/src/main/res/font/roboto_thin_italic.ttf similarity index 100% rename from app/src/main/res/font/roboto_thin_italic.ttf rename to core/designsystem/src/main/res/font/roboto_thin_italic.ttf diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/core/designsystem/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to core/designsystem/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/mipmap-hdpi/ic_launcher.png rename to core/designsystem/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/mipmap-mdpi/ic_launcher.png rename to core/designsystem/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/core/designsystem/src/main/res/values-ru/strings.xml b/core/designsystem/src/main/res/values-ru/strings.xml new file mode 100644 index 00000000..02888443 --- /dev/null +++ b/core/designsystem/src/main/res/values-ru/strings.xml @@ -0,0 +1,90 @@ + + + Вложение + Вложения + Настройки + Произошла ошибка + Ошибка: %s + Ошибка + Пароль + Код капчи + Введите код с картинки + Логин + Поле не должно быть пустым + Код + Введите код из смс + Вы + Геолокация + Точка + Сообщение + Сообщения + Нет сообщений + Сообщение самоуничтожилось + Обновить + Вчера + Сегодня + Г + М + Н + Д + Сейчас + Начните печатать здесь… + Введите логин + Введите пароль + Введите код + Нужна валидация + Произошла неизвестная ошибка + Ошибка авторизации + Токен недействителен + %s создал(-а) «%s» + %s переименовал(-а) чат в «%s» + %s обновил(-а) фото беседы + %s удалил(-а) фото беседы + %s вышел(-ла) из чата + %s исключил %s + %s вернулся(-лась) в беседу + %s пригласил(-а) %s + %s присоединился(-лась) в беседу через ссылку + %s присоединился(-лась) к звонку + %s присоединился(-лась) к звонку через ссылку + %s закрепил(-а) сообщение + %s открепил(-а) сообщение + %s сделал(-а) скриншот беседы + %s изменил(-а) тему беседы + Фотография + %d фотографии + %d фотографий + %d фотографий + Видео + Аудиозапись + Файл + Голосовое сообщение + Ссылка + Мини-приложение + Стикер + Подарок + Запись на стене + Граффити + Опрос + Комментарий + Звонок + Текущий звонок + Событие + Куратор + История + Виджет + Место + Запись сообщества + Запись пользователя + Запись на стене + Выйти + Подтверждение + Динамические цвета + Цвета для приложения будут извлечены из ваших обоев на главном экране + Язык приложения + Текущий: %s + Системный + Применить + Открыть системный пикер языка + Язык приложения + diff --git a/app/src/main/res/values/arrays.xml b/core/designsystem/src/main/res/values-uk/strings.xml similarity index 60% rename from app/src/main/res/values/arrays.xml rename to core/designsystem/src/main/res/values-uk/strings.xml index 0d2c4cc4..3ea04e70 100644 --- a/app/src/main/res/values/arrays.xml +++ b/core/designsystem/src/main/res/values-uk/strings.xml @@ -1,4 +1,2 @@ - - - \ No newline at end of file + diff --git a/app/src/main/res/values/ic_launcher_background.xml b/core/designsystem/src/main/res/values/ic_launcher_background.xml similarity index 100% rename from app/src/main/res/values/ic_launcher_background.xml rename to core/designsystem/src/main/res/values/ic_launcher_background.xml diff --git a/app/src/main/res/values/plurals.xml b/core/designsystem/src/main/res/values/plurals.xml similarity index 100% rename from app/src/main/res/values/plurals.xml rename to core/designsystem/src/main/res/values/plurals.xml diff --git a/app/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml similarity index 75% rename from app/src/main/res/values/strings.xml rename to core/designsystem/src/main/res/values/strings.xml index 34a0556d..6209e291 100644 --- a/app/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -1,15 +1,10 @@ Fast + Fast Messenger + Attachment Attachments - Friends - Chats - Profile - Favorites - Settings - Static Settings - Error occurred Error: %s @@ -17,18 +12,17 @@ Password - Log in - Captcha code Input code from picture Login + Field must not be empty + Code Input code from sms - You Geolocation @@ -42,15 +36,6 @@ Messages self-destructed - Yesterday - - Today - - Y - M - W - D - Now Start typing here… Input login Input password @@ -99,7 +84,7 @@ %d files - Voice message + Voice message Link Mini App Sticker @@ -115,6 +100,18 @@ Story Widget Place + Artist + Playlist + Podcast + + Uploading file + Uploading photo + Uploading video + Typing + Recording + + %s are typing + %s is typing %d bytes @@ -140,6 +137,7 @@ Delete for all Mark as spam + Mark as read Delete Delete @@ -219,4 +217,43 @@ Value + Dark theme + Enabled + Follow system + Follow battery saver + Disabled + Current value: %s + Unknown + AMOLED dark theme + Use AMOLED theme with a pure black background when dark theme is enabled + + Dynamic colors + The colors for the app will be extracted from your home screen wallpaper + + Application Language + Current: %s + + System + English + Russian + Ukrainian + + System + English + Русский + Українська + + Apply + Open system language picker + Application language + Refresh + Members: %d + Loading… + Conversations + Friends + Profile + All + Online + No items + diff --git a/core/designsystem/src/main/res/values/themes.xml b/core/designsystem/src/main/res/values/themes.xml new file mode 100644 index 00000000..46d12ce8 --- /dev/null +++ b/core/designsystem/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +