From 8d0cd19322cd1f4bf8059a9675547674477e851d Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Fri, 25 Mar 2022 14:20:12 -0700 Subject: [PATCH] Code saving and stuff (#9) lil update --- app/build.gradle.kts | 70 ++- app/src/main/AndroidManifest.xml | 6 +- .../com/meloda/fast/activity/MainActivity.kt | 45 +- .../kotlin/com/meloda/fast/api/ApiEvent.kt | 28 + .../com/meloda/fast/api/LongPollEvent.kt | 20 + .../meloda/fast/api/LongPollUpdatesParser.kt | 275 +++++++++ .../kotlin/com/meloda/fast/api/VKConstants.kt | 27 +- .../kotlin/com/meloda/fast/api/VkUtils.kt | 163 +++--- .../meloda/fast/api/model/VkConversation.kt | 4 +- .../com/meloda/fast/api/model/VkMessage.kt | 10 +- .../fast/api/model/attachments/VkPhoto.kt | 25 +- .../fast/api/model/attachments/VkStory.kt | 10 +- .../fast/api/model/attachments/VkVideo.kt | 52 +- .../api/model/attachments/VkVoiceMessage.kt | 4 +- .../fast/api/model/attachments/VkWidget.kt | 8 + .../fast/api/model/base/BaseVkMessage.kt | 10 +- .../base/attachments/BaseVkAttachmentItem.kt | 48 +- .../api/model/base/attachments/BaseVkEvent.kt | 6 +- .../model/base/attachments/BaseVkGroupCall.kt | 3 + .../model/base/attachments/BaseVkMiniApp.kt | 3 + .../api/model/base/attachments/BaseVkPoll.kt | 4 +- .../api/model/base/attachments/BaseVkStory.kt | 4 +- .../api/model/base/attachments/BaseVkVideo.kt | 16 +- .../base/attachments/BaseVkVoiceMessage.kt | 4 +- .../model/base/attachments/BaseVkWallReply.kt | 4 +- .../model/base/attachments/BaseVkWidget.kt | 11 + .../fast/api/network/AuthInterceptor.kt | 11 +- .../com/meloda/fast/api/network/VkUrls.kt | 30 + .../api/network/account/AccountDataSource.kt | 15 + .../fast/api/network/account/AccountRepo.kt | 17 + .../api/network/account/AccountRequests.kt | 20 + .../fast/api/network/account/AccountUrls.kt | 10 + .../fast/api/network/auth/AuthDataSource.kt | 2 +- .../meloda/fast/api/network/auth/AuthRepo.kt | 4 +- .../fast/api/network/auth/AuthRequest.kt | 58 +- .../fast/api/network/auth/AuthResponse.kt | 4 +- .../fast/api/network/longpoll/LongPollRepo.kt | 11 +- .../api/network/longpoll/LongPollRequests.kt | 24 + .../network/messages/MessagesDataSource.kt | 36 +- .../fast/api/network/messages/MessagesRepo.kt | 4 + .../api/network/messages/MessagesRequest.kt | 21 +- .../api/network/messages/MessagesResponse.kt | 10 +- .../fast/api/network/messages/MessagesUrls.kt | 1 + .../com/meloda/fast/base/BaseActivity.kt | 30 +- .../com/meloda/fast/base/ResourceManager.kt | 20 + .../meloda/fast/base/adapter/BaseAdapter.kt | 221 ++++--- .../fast/base/adapter/EmptyHeaderAdapter.kt | 3 +- .../com/meloda/fast/base/adapter/Items.kt | 15 - .../com/meloda/fast/base/viewmodel/Events.kt | 6 +- .../com/meloda/fast/common/AppGlobal.kt | 10 +- .../kotlin/com/meloda/fast/common/Screens.kt | 17 + .../com/meloda/fast/database/AppDatabase.kt | 2 +- .../com/meloda/fast/database/Converters.kt | 21 +- .../com/meloda/fast/di/NavigationModule.kt | 25 + .../com/meloda/fast/di/NetworkModule.kt | 38 +- .../kotlin/com/meloda/fast/extensions/Ext.kt | 87 +++ .../com/meloda/fast/extensions/GlideExt.kt | 139 +++++ .../fast/extensions/NavigationExtensions.kt | 266 --------- .../fast/extensions/TextViewExtensions.kt | 11 - .../com/meloda/fast/model/ListModels.kt | 13 + .../com/meloda/fast/model/SelectableItem.kt | 22 + .../conversations/ConversationsAdapter.kt | 117 ++-- .../conversations/ConversationsFragment.kt | 154 ++--- .../ConversationsResourceManager.kt | 19 + .../conversations/ConversationsViewModel.kt | 72 ++- .../fast/screens/login/LoginFragment.kt | 44 +- .../fast/screens/login/LoginViewModel.kt | 50 +- .../meloda/fast/screens/main/MainFragment.kt | 40 +- .../meloda/fast/screens/main/MainViewModel.kt | 27 +- .../screens/messages/AttachmentInflater.kt | 339 ++++++----- .../messages/MessagesHistoryAdapter.kt | 187 +++--- .../messages/MessagesHistoryFragment.kt | 255 +++++--- .../messages/MessagesHistoryViewModel.kt | 56 +- .../screens/messages/MessagesPreparator.kt | 42 +- .../meloda/fast/service/LongPollService.kt | 50 -- .../fast/service/MessagesUpdateService.kt | 182 ++++++ .../com/meloda/fast/service/OnlineService.kt | 75 +++ .../com/meloda/fast/util/AndroidUtils.kt | 17 +- .../com/meloda/fast/widget/NoItemsView.kt | 18 +- ..._message_attachment_story_image_dimmer.xml | 10 + .../drawable/ic_message_out_background.xml | 4 +- .../ic_message_out_background_middle.xml | 4 +- ...c_message_out_background_middle_stroke.xml | 2 +- .../ic_message_out_background_stroke.xml | 2 +- app/src/main/res/drawable/ic_online_pc.xml | 2 +- .../res/drawable/ic_round_arrow_back_24.xml | 9 + app/src/main/res/layout/activity_main.xml | 24 +- app/src/main/res/layout/dialog_captcha.xml | 140 +++-- .../main/res/layout/dialog_message_delete.xml | 31 +- app/src/main/res/layout/dialog_validation.xml | 128 ++-- .../res/layout/fragment_conversations.xml | 198 +++---- app/src/main/res/layout/fragment_login.xml | 241 ++++---- app/src/main/res/layout/fragment_main.xml | 27 - .../res/layout/fragment_messages_history.xml | 551 +++++++++--------- app/src/main/res/layout/item_conversation.xml | 442 +++++++------- .../layout/item_message_attachment_audio.xml | 90 ++- .../layout/item_message_attachment_call.xml | 89 ++- .../layout/item_message_attachment_file.xml | 90 ++- .../layout/item_message_attachment_gift.xml | 21 +- .../item_message_attachment_graffiti.xml | 21 +- .../layout/item_message_attachment_link.xml | 105 ++-- .../layout/item_message_attachment_photo.xml | 32 + .../item_message_attachment_sticker.xml | 21 +- .../layout/item_message_attachment_story.xml | 52 ++ .../layout/item_message_attachment_video.xml | 46 ++ .../layout/item_message_attachment_voice.xml | 95 ++- .../item_message_attachment_wall_post.xml | 96 ++- app/src/main/res/layout/item_message_in.xml | 156 +++-- app/src/main/res/layout/item_message_out.xml | 108 ++-- .../main/res/layout/item_message_service.xml | 56 +- .../main/res/menu/activity_main_bottom.xml | 2 +- app/src/main/res/navigation/login.xml | 23 - app/src/main/res/navigation/main.xml | 36 -- app/src/main/res/navigation/messages.xml | 37 -- app/src/main/res/navigation/nav_graph.xml | 8 - app/src/main/res/values-night-v31/colors.xml | 26 - app/src/main/res/values-night/bools.xml | 7 + app/src/main/res/values-night/colors.xml | 49 +- app/src/main/res/values-night/themes.xml | 40 -- app/src/main/res/values-v31/colors.xml | 31 - app/src/main/res/values/attrs.xml | 18 +- app/src/main/res/values/bools.xml | 7 + app/src/main/res/values/colors.xml | 85 ++- app/src/main/res/values/dimens.xml | 3 + .../res/values/ic_launcher_background.xml | 1 - app/src/main/res/values/monet_colors.xml | 50 ++ app/src/main/res/values/strings.xml | 8 +- app/src/main/res/values/themes.xml | 83 ++- build.gradle.kts | 7 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 130 files changed, 4189 insertions(+), 3055 deletions(-) create mode 100644 app/src/main/kotlin/com/meloda/fast/api/ApiEvent.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/api/LongPollEvent.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/api/LongPollUpdatesParser.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWidget.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWidget.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/account/AccountDataSource.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/account/AccountRepo.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/account/AccountRequests.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/account/AccountUrls.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/longpoll/LongPollRequests.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/base/ResourceManager.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/base/adapter/Items.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/common/Screens.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/di/NavigationModule.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/extensions/Ext.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/extensions/GlideExt.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/extensions/NavigationExtensions.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/extensions/TextViewExtensions.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/model/ListModels.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/model/SelectableItem.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsResourceManager.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/service/LongPollService.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/service/MessagesUpdateService.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/service/OnlineService.kt create mode 100644 app/src/main/res/drawable/ic_message_attachment_story_image_dimmer.xml create mode 100644 app/src/main/res/drawable/ic_round_arrow_back_24.xml delete mode 100644 app/src/main/res/layout/fragment_main.xml create mode 100644 app/src/main/res/layout/item_message_attachment_photo.xml create mode 100644 app/src/main/res/layout/item_message_attachment_story.xml create mode 100644 app/src/main/res/layout/item_message_attachment_video.xml delete mode 100644 app/src/main/res/navigation/login.xml delete mode 100644 app/src/main/res/navigation/main.xml delete mode 100644 app/src/main/res/navigation/messages.xml delete mode 100644 app/src/main/res/navigation/nav_graph.xml delete mode 100644 app/src/main/res/values-night-v31/colors.xml create mode 100644 app/src/main/res/values-night/bools.xml delete mode 100644 app/src/main/res/values-night/themes.xml delete mode 100644 app/src/main/res/values-v31/colors.xml create mode 100644 app/src/main/res/values/bools.xml create mode 100644 app/src/main/res/values/monet_colors.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0117e214..cf381ee8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,14 +1,17 @@ import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -val login: String = gradleLocalProperties(rootDir).getProperty("vklogin") -val password: String = gradleLocalProperties(rootDir).getProperty("vkpassword") +val login: String = gradleLocalProperties(rootDir).getProperty("vkLogin") +val password: String = gradleLocalProperties(rootDir).getProperty("vkPassword") + +val sdkPackage: String = gradleLocalProperties(rootDir).getProperty("sdkPackage") +val sdkFingerprint: String = gradleLocalProperties(rootDir).getProperty("sdkFingerprint") plugins { id("com.android.application") id("kotlin-android") id("kotlin-kapt") id("kotlin-parcelize") - id("androidx.navigation.safeargs.kotlin") id("dagger.hilt.android.plugin") } @@ -34,6 +37,9 @@ android { getByName("debug") { buildConfigField("String", "vkLogin", login) buildConfigField("String", "vkPassword", password) + + buildConfigField("String", "sdkPackage", sdkPackage) + buildConfigField("String", "sdkFingerprint", sdkFingerprint) } getByName("release") { isMinifyEnabled = false @@ -41,6 +47,9 @@ android { buildConfigField("String", "vkLogin", login) buildConfigField("String", "vkPassword", password) + buildConfigField("String", "sdkPackage", sdkPackage) + buildConfigField("String", "sdkFingerprint", sdkFingerprint) + proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" @@ -49,23 +58,13 @@ android { } compileOptions { - isCoreLibraryDesugaringEnabled = true - sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - tasks.withType { - kotlinOptions { - jvmTarget = "1.8" - } - } - buildFeatures { - dataBinding = true viewBinding = true } - } kapt { @@ -78,44 +77,42 @@ kapt { } dependencies { - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.31") + // Cicerone - Navigation + implementation("com.github.terrakok:cicerone:7.1") - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") + implementation("androidx.constraintlayout:constraintlayout:2.1.3") implementation("com.github.massoudss:waveformSeekBar:3.1.0") - implementation("androidx.core:core-splashscreen:1.0.0-alpha02") + implementation("androidx.core:core-splashscreen:1.0.0-beta02") - implementation("androidx.work:work-runtime-ktx:2.6.0") + implementation("androidx.work:work-runtime-ktx:2.7.1") implementation("androidx.datastore:datastore-preferences:1.0.0") - implementation("androidx.paging:paging-runtime-ktx:3.0.1") + implementation("androidx.paging:paging-runtime-ktx:3.1.1") - implementation("androidx.appcompat:appcompat:1.4.0-beta01") - implementation("com.google.android.material:material:1.5.0-alpha04") - implementation("androidx.core:core-ktx:1.7.0-beta02") - implementation("androidx.preference:preference-ktx:1.1.1") + implementation("androidx.appcompat:appcompat:1.4.1") + implementation("com.google.android.material:material:1.6.0-beta01") + implementation("androidx.core:core-ktx:1.7.0") + implementation("androidx.preference:preference-ktx:1.2.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01") implementation("androidx.recyclerview:recyclerview:1.2.1") implementation("androidx.cardview:cardview:1.0.0") - implementation("androidx.fragment:fragment-ktx:1.3.6") + implementation("androidx.fragment:fragment-ktx:1.4.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2") - implementation("androidx.room:room-ktx:2.3.0") - implementation("androidx.room:room-runtime:2.3.0") - kapt("androidx.room:room-compiler:2.3.0") + implementation("androidx.room:room-ktx:2.4.2") + implementation("androidx.room:room-runtime:2.4.2") + kapt("androidx.room:room-compiler:2.4.2") - implementation("androidx.navigation:navigation-fragment-ktx:2.3.5") - implementation("androidx.navigation:navigation-ui-ktx:2.3.5") - - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.3.1") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1") - implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1") - implementation("androidx.lifecycle:lifecycle-common-java8:2.3.1") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.1") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.1") + implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.4.1") + implementation("androidx.lifecycle:lifecycle-common-java8:2.4.1") implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.2") implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.2") @@ -124,9 +121,8 @@ dependencies { implementation("com.google.dagger:hilt-android:2.39.1") kapt("com.google.dagger:hilt-android-compiler:2.39.1") - implementation("androidx.hilt:hilt-navigation-fragment:1.0.0") - implementation("com.github.yogacp:android-viewbinding:1.0.3") + implementation("com.github.yogacp:android-viewbinding:1.0.4") implementation("io.coil-kt:coil:1.4.0") @@ -134,4 +130,6 @@ dependencies { implementation("org.jsoup:jsoup:1.14.3") implementation("ch.acra:acra:4.11.1") + implementation("com.github.bumptech.glide:glide:4.13.0") + kapt("com.github.bumptech.glide:compiler:4.13.0") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4e07d606..c427ab88 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,7 +17,6 @@ android:testOnly="false" android:theme="@style/AppTheme" tools:replace="android:allowBackup"> - + + , + val groups: HashMap + ) : LongPollEvent() + + data class VkMessageEditEvent(val message: VkMessage) : LongPollEvent() + + data class VkMessageReadIncomingEvent(val peerId: Int, val messageId: Int) : LongPollEvent() + data class VkMessageReadOutgoingEvent(val peerId: Int, val messageId: Int) : LongPollEvent() + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/LongPollUpdatesParser.kt b/app/src/main/kotlin/com/meloda/fast/api/LongPollUpdatesParser.kt new file mode 100644 index 00000000..8889e4d8 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/LongPollUpdatesParser.kt @@ -0,0 +1,275 @@ +package com.meloda.fast.api + +import android.util.Log +import com.google.gson.JsonArray +import com.meloda.fast.api.model.VkGroup +import com.meloda.fast.api.model.VkUser +import com.meloda.fast.api.network.Answer +import com.meloda.fast.api.network.messages.MessagesDataSource +import com.meloda.fast.api.network.messages.MessagesGetByIdRequest +import com.meloda.fast.base.viewmodel.VkEventCallback +import kotlinx.coroutines.* +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +@Suppress("UNCHECKED_CAST") +class LongPollUpdatesParser( + private val messagesDataSource: MessagesDataSource +) : CoroutineScope { + + companion object { + private const val TAG = "LongPollUpdatesParser" + } + + private val job = SupervisorJob() + + private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + Log.d(TAG, "error: $throwable") + throwable.printStackTrace() + } + + override val coroutineContext: CoroutineContext + get() = Dispatchers.Default + job + exceptionHandler + + private val listenersMap: MutableMap>> = + mutableMapOf() + + fun parseNextUpdate(event: JsonArray) { + val eventType: ApiEvent? = + try { + ApiEvent.parse(event[0].asInt) + } catch (e: Exception) { + null + } + + if (eventType != null) { + println("$TAG: $eventType: $event") + } else { + println("$TAG: unknown event: $event") + } + + 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.FRIEND_ONLINE -> parseFriendOnline(eventType, event) + ApiEvent.FRIEND_OFFLINE -> parseFriendOffline(eventType, event) + ApiEvent.MESSAGES_DELETED -> parseMessagesDeleted(eventType, event) +// ApiEvent.PIN_UNPIN_CONVERSATION -> TODO() +// ApiEvent.TYPING -> TODO() +// ApiEvent.VOICE_RECORDING -> TODO() +// ApiEvent.PHOTO_UPLOADING -> TODO() +// ApiEvent.VIDEO_UPLOADING -> TODO() +// ApiEvent.FILE_UPLOADING -> TODO() +// ApiEvent.UNREAD_COUNT_UPDATE -> TODO() + } + + } + + private fun parseMessageSetFlags(eventType: ApiEvent, event: JsonArray) { +// println("$TAG: $eventType: $event") + } + + private fun parseMessageClearFlags(eventType: ApiEvent, event: JsonArray) { +// println("$TAG: $eventType: $event") + } + + private fun parseMessageNew(eventType: ApiEvent, event: JsonArray) { +// println("$TAG: $eventType: $event") + + val messageId = event[1].asInt + + launch { + val newMessageEvent: LongPollEvent.VkMessageNewEvent = + loadNormalMessage( + eventType, + messageId + ) + + listenersMap[ApiEvent.MESSAGE_NEW]?.let { + it.map { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent(newMessageEvent) + } + } + } + } + + private fun parseMessageEdit(eventType: ApiEvent, event: JsonArray) { +// println("$TAG: $eventType: $event") + + val messageId = event[1].asInt + + launch { + val editedMessageEvent: LongPollEvent.VkMessageEditEvent = + loadNormalMessage( + eventType, + messageId + ) + + listenersMap[ApiEvent.MESSAGE_EDIT]?.let { + it.map { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent(editedMessageEvent) + } + } + } + } + + private fun parseMessageReadIncoming(eventType: ApiEvent, event: JsonArray) { + val peerId = event[1].asInt + val messageId = event[2].asInt + + launch { + listenersMap[ApiEvent.MESSAGE_READ_INCOMING]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollEvent.VkMessageReadIncomingEvent( + peerId = peerId, + messageId = messageId + ) + ) + } + } + } + } + + private fun parseMessageReadOutgoing(eventType: ApiEvent, event: JsonArray) { + val peerId = event[1].asInt + val messageId = event[2].asInt + + launch { + listenersMap[ApiEvent.MESSAGE_READ_OUTGOING]?.let { listeners -> + listeners.map { vkEventCallback -> + (vkEventCallback as VkEventCallback) + .onEvent( + LongPollEvent.VkMessageReadOutgoingEvent( + peerId = peerId, + messageId = messageId + ) + ) + } + } + } + } + + private fun parseFriendOnline(eventType: ApiEvent, event: JsonArray) { +// println("$TAG: $eventType: $event") + } + + private fun parseFriendOffline(eventType: ApiEvent, event: JsonArray) { +// println("$TAG: $eventType: $event") + } + + private fun parseMessagesDeleted(eventType: ApiEvent, event: JsonArray) { +// println("$TAG: $eventType: $event") + } + + private suspend fun loadNormalMessage(eventType: ApiEvent, messageId: Int) = + coroutineScope { + suspendCoroutine { + launch { + val normalMessageResponse = messagesDataSource.getById( + MessagesGetByIdRequest( + messagesIds = listOf(messageId), + extended = true, + fields = VKConstants.ALL_FIELDS + ) + ) + + if (normalMessageResponse !is Answer.Success) { + (normalMessageResponse as Answer.Error).throwable.let { throw it } + } + + val messagesResponse = normalMessageResponse.data.response ?: return@launch + + val messagesList = messagesResponse.items + if (messagesList.isEmpty()) return@launch + + val normalMessage = messagesList[0].asVkMessage() + messagesDataSource.store(listOf(normalMessage)) + + val profiles = hashMapOf() + messagesResponse.profiles?.forEach { baseUser -> + baseUser.asVkUser().let { user -> profiles[user.id] = user } + } + + val groups = hashMapOf() + messagesResponse.groups?.forEach { baseGroup -> + baseGroup.asVkGroup().let { group -> groups[group.id] = group } + } + + val resumeValue: LongPollEvent? = when (eventType) { + ApiEvent.MESSAGE_NEW -> + LongPollEvent.VkMessageNewEvent( + normalMessage, + profiles, + groups + ) + ApiEvent.MESSAGE_EDIT -> LongPollEvent.VkMessageEditEvent(normalMessage) + else -> null + } + + resumeValue?.let { value -> it.resume(value as T) } + } + } + } + + + fun registerListener(eventType: ApiEvent, listener: VkEventCallback) { + listenersMap.let { map -> + map[eventType] = (map[eventType] ?: mutableListOf()).also { + it.add(listener) + } + } + } + + 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 clearListeners() { + listenersMap.clear() + } +} + +internal inline fun assembleEventCallback( + crossinline block: (R) -> Unit +): VkEventCallback { + return object : VkEventCallback { + override fun onEvent(event: R) = block.invoke(event) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/VKConstants.kt b/app/src/main/kotlin/com/meloda/fast/api/VKConstants.kt index 90d32bf9..325148f8 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/VKConstants.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/VKConstants.kt @@ -2,6 +2,7 @@ package com.meloda.fast.api import com.meloda.fast.api.model.attachments.* +@Suppress("RemoveExplicitTypeArguments") object VKConstants { const val GROUP_FIELDS = "description,members_count,counters,status,verified" @@ -12,10 +13,13 @@ object VKConstants { const val ALL_FIELDS = "$USER_FIELDS,$GROUP_FIELDS" const val API_VERSION = "5.132" + const val LP_VERSION = 10 + const val VK_APP_ID = "2274003" const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH" const val FAST_GROUP_ID = -119516304 + const val FAST_APP_ID = "6964679" object Auth { const val SCOPE = "notify," + @@ -38,7 +42,7 @@ object VKConstants { } } - val restrictedToEditAttachments = listOf( + val restrictedToEditAttachments = listOf>( VkCall::class.java, VkCurator::class.java, VkEvent::class.java, @@ -46,6 +50,25 @@ object VKConstants { VkGraffiti::class.java, VkGroupCall::class.java, VkStory::class.java, - VkVoiceMessage::class.java + VkVoiceMessage::class.java, + VkWidget::class.java + ) + + val separatedFromTextAttachments = listOf>( + VkPhoto::class.java, + VkVideo::class.java, + VkSticker::class.java, + VkStory::class.java, + VkWidget::class.java, + VkGroupCall::class.java, + VkGroupCall::class.java, + VkCurator::class.java, + VkEvent::class.java, + VkGift::class.java, + VkGraffiti::class.java, + VkPoll::class.java, + VkWall::class.java, + VkWallReply::class.java, + VkLink::class.java ) } \ 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 index d6c4d7ce..e357eb5d 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt @@ -139,86 +139,82 @@ object VkUtils { for (baseAttachment in baseAttachments) { when (baseAttachment.getPreparedType()) { - BaseVkAttachmentItem.AttachmentType.PHOTO -> { + BaseVkAttachmentItem.AttachmentType.Photo -> { val photo = baseAttachment.photo ?: continue attachments += photo.asVkPhoto() } - BaseVkAttachmentItem.AttachmentType.VIDEO -> { + BaseVkAttachmentItem.AttachmentType.Video -> { val video = baseAttachment.video ?: continue attachments += video.asVkVideo() } - BaseVkAttachmentItem.AttachmentType.AUDIO -> { + BaseVkAttachmentItem.AttachmentType.Audio -> { val audio = baseAttachment.audio ?: continue attachments += audio.asVkAudio() } - BaseVkAttachmentItem.AttachmentType.FILE -> { + BaseVkAttachmentItem.AttachmentType.File -> { val file = baseAttachment.file ?: continue attachments += file.asVkFile() } - BaseVkAttachmentItem.AttachmentType.LINK -> { + BaseVkAttachmentItem.AttachmentType.Link -> { val link = baseAttachment.link ?: continue attachments += link.asVkLink() } - BaseVkAttachmentItem.AttachmentType.MINI_APP -> { + BaseVkAttachmentItem.AttachmentType.MiniApp -> { val miniApp = baseAttachment.miniApp ?: continue - attachments += VkMiniApp( - link = miniApp.app.shareUrl - ) + attachments += miniApp.asVkMiniApp() } - BaseVkAttachmentItem.AttachmentType.VOICE -> { + BaseVkAttachmentItem.AttachmentType.Voice -> { val voiceMessage = baseAttachment.voiceMessage ?: continue attachments += voiceMessage.asVkVoiceMessage() } - BaseVkAttachmentItem.AttachmentType.STICKER -> { + BaseVkAttachmentItem.AttachmentType.Sticker -> { val sticker = baseAttachment.sticker ?: continue attachments += sticker.asVkSticker() } - BaseVkAttachmentItem.AttachmentType.GIFT -> { + BaseVkAttachmentItem.AttachmentType.Gift -> { val gift = baseAttachment.gift ?: continue attachments += gift.asVkGift() } - BaseVkAttachmentItem.AttachmentType.WALL -> { + BaseVkAttachmentItem.AttachmentType.Wall -> { val wall = baseAttachment.wall ?: continue attachments += wall.asVkWall() } - BaseVkAttachmentItem.AttachmentType.GRAFFITI -> { + BaseVkAttachmentItem.AttachmentType.Graffiti -> { val graffiti = baseAttachment.graffiti ?: continue attachments += graffiti.asVkGraffiti() } - BaseVkAttachmentItem.AttachmentType.POLL -> { + BaseVkAttachmentItem.AttachmentType.Poll -> { val poll = baseAttachment.poll ?: continue - attachments += VkPoll( - id = poll.id - ) + attachments += poll.asVkPoll() } - BaseVkAttachmentItem.AttachmentType.WALL_REPLY -> { + BaseVkAttachmentItem.AttachmentType.WallReply -> { val wallReply = baseAttachment.wallReply ?: continue - attachments += VkWallReply( - id = wallReply.id - ) + attachments += wallReply.asVkWallReply() } - BaseVkAttachmentItem.AttachmentType.CALL -> { + BaseVkAttachmentItem.AttachmentType.Call -> { val call = baseAttachment.call ?: continue attachments += call.asVkCall() } - BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS -> { + BaseVkAttachmentItem.AttachmentType.GroupCallInProgress -> { val groupCall = baseAttachment.groupCall ?: continue - attachments += VkGroupCall( - initiatorId = groupCall.initiator_id - ) + attachments += groupCall.asVkGroupCall() } - BaseVkAttachmentItem.AttachmentType.CURATOR -> { + BaseVkAttachmentItem.AttachmentType.Curator -> { val curator = baseAttachment.curator ?: continue attachments += curator.asVkCurator() } - BaseVkAttachmentItem.AttachmentType.EVENT -> { + BaseVkAttachmentItem.AttachmentType.Event -> { val event = baseAttachment.event ?: continue attachments += event.asVkEvent() } - BaseVkAttachmentItem.AttachmentType.STORY -> { + BaseVkAttachmentItem.AttachmentType.Story -> { val story = baseAttachment.story ?: continue attachments += story.asVkStory() } + BaseVkAttachmentItem.AttachmentType.Widget -> { + val widget = baseAttachment.widget ?: continue + attachments += widget.asVkWidget() + } else -> continue } } @@ -580,22 +576,22 @@ object VkUtils { 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.MINI_APP -> 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.WALL_REPLY -> R.drawable.ic_attachment_wall_reply - BaseVkAttachmentItem.AttachmentType.CALL -> R.drawable.ic_attachment_call - BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS -> R.drawable.ic_attachment_group_call - BaseVkAttachmentItem.AttachmentType.STORY -> R.drawable.ic_attachment_story + 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 } @@ -617,24 +613,25 @@ object VkUtils { 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.MINI_APP - 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.WALL_REPLY - is VkCall -> BaseVkAttachmentItem.AttachmentType.CALL - is VkGroupCall -> BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS - is VkEvent -> BaseVkAttachmentItem.AttachmentType.EVENT - is VkCurator -> BaseVkAttachmentItem.AttachmentType.CURATOR - is VkStory -> BaseVkAttachmentItem.AttachmentType.STORY + 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 } } @@ -645,42 +642,44 @@ object VkUtils { size: Int = 1 ): String { return when (attachmentType) { - BaseVkAttachmentItem.AttachmentType.PHOTO -> + BaseVkAttachmentItem.AttachmentType.Photo -> context.resources.getQuantityString(R.plurals.attachment_photos, size, size) - BaseVkAttachmentItem.AttachmentType.VIDEO -> + BaseVkAttachmentItem.AttachmentType.Video -> context.resources.getQuantityString(R.plurals.attachment_videos, size, size) - BaseVkAttachmentItem.AttachmentType.AUDIO -> + BaseVkAttachmentItem.AttachmentType.Audio -> context.resources.getQuantityString(R.plurals.attachment_audios, size, size) - BaseVkAttachmentItem.AttachmentType.FILE -> + BaseVkAttachmentItem.AttachmentType.File -> context.resources.getQuantityString(R.plurals.attachment_files, size, size) - BaseVkAttachmentItem.AttachmentType.LINK -> + BaseVkAttachmentItem.AttachmentType.Link -> context.resources.getString(R.string.message_attachments_link) - BaseVkAttachmentItem.AttachmentType.VOICE -> + BaseVkAttachmentItem.AttachmentType.Voice -> context.resources.getString(R.string.message_attachments_voice) - BaseVkAttachmentItem.AttachmentType.MINI_APP -> + BaseVkAttachmentItem.AttachmentType.MiniApp -> context.resources.getString(R.string.message_attachments_mini_app) - BaseVkAttachmentItem.AttachmentType.STICKER -> + BaseVkAttachmentItem.AttachmentType.Sticker -> context.resources.getString(R.string.message_attachments_sticker) - BaseVkAttachmentItem.AttachmentType.GIFT -> + BaseVkAttachmentItem.AttachmentType.Gift -> context.resources.getString(R.string.message_attachments_gift) - BaseVkAttachmentItem.AttachmentType.WALL -> + BaseVkAttachmentItem.AttachmentType.Wall -> context.resources.getString(R.string.message_attachments_wall) - BaseVkAttachmentItem.AttachmentType.GRAFFITI -> + BaseVkAttachmentItem.AttachmentType.Graffiti -> context.resources.getString(R.string.message_attachments_graffiti) - BaseVkAttachmentItem.AttachmentType.POLL -> + BaseVkAttachmentItem.AttachmentType.Poll -> context.resources.getString(R.string.message_attachments_poll) - BaseVkAttachmentItem.AttachmentType.WALL_REPLY -> + BaseVkAttachmentItem.AttachmentType.WallReply -> context.resources.getString(R.string.message_attachments_wall_reply) - BaseVkAttachmentItem.AttachmentType.CALL -> + BaseVkAttachmentItem.AttachmentType.Call -> context.resources.getString(R.string.message_attachments_call) - BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS -> + BaseVkAttachmentItem.AttachmentType.GroupCallInProgress -> context.resources.getString(R.string.message_attachments_call_in_progress) - BaseVkAttachmentItem.AttachmentType.EVENT -> + BaseVkAttachmentItem.AttachmentType.Event -> context.resources.getString(R.string.message_attachments_event) - BaseVkAttachmentItem.AttachmentType.CURATOR -> + BaseVkAttachmentItem.AttachmentType.Curator -> context.resources.getString(R.string.message_attachments_curator) - BaseVkAttachmentItem.AttachmentType.STORY -> + BaseVkAttachmentItem.AttachmentType.Story -> context.resources.getString(R.string.message_attachments_story) + BaseVkAttachmentItem.AttachmentType.Widget -> + context.resources.getString(R.string.message_attachments_widget) else -> attachmentType.value } } diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/VkConversation.kt b/app/src/main/kotlin/com/meloda/fast/api/model/VkConversation.kt index c6673448..c46bba0d 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/VkConversation.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/VkConversation.kt @@ -1,11 +1,11 @@ package com.meloda.fast.api.model -import android.os.Parcelable import androidx.lifecycle.MutableLiveData import androidx.room.Embedded import androidx.room.Entity import androidx.room.Ignore import androidx.room.PrimaryKey +import com.meloda.fast.model.SelectableItem import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @@ -35,7 +35,7 @@ data class VkConversation( @Embedded(prefix = "lastMessage_") var lastMessage: VkMessage? = null, -) : Parcelable { +) : SelectableItem(id) { @Ignore @IgnoredOnParcel 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 index 83f8d229..513c5b31 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/VkMessage.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/VkMessage.kt @@ -7,7 +7,7 @@ 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.base.adapter.SelectableItem +import com.meloda.fast.model.SelectableItem import com.meloda.fast.util.TimeUtils import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @@ -33,10 +33,8 @@ data class VkMessage( var forwards: List? = null, var attachments: List? = null, - -// @Embedded(prefix = "replyMessage_") var replyMessage: VkMessage? = null -) : SelectableItem() { +) : SelectableItem(id) { @Ignore @IgnoredOnParcel @@ -53,8 +51,8 @@ data class VkMessage( fun isGroup() = fromId < 0 fun isRead(conversation: VkConversation) = - if (isOut) conversation.outRead < id - else conversation.inRead < id + if (isOut) conversation.outRead - id >= 0 + else conversation.inRead - id >= 0 fun getPreparedAction(): Action? { if (action == null) return null diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPhoto.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPhoto.kt index 8a661d4b..ef95a6f0 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPhoto.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPhoto.kt @@ -20,21 +20,30 @@ data class VkPhoto( val userId: Int? ) : VkAttachment() { + companion object { + const val SIZE_TYPE_75 = 's' + const val SIZE_TYPE_130 = 'm' + const val SIZE_TYPE_604 = 'x' + const val SIZE_TYPE_807 = 'y' + const val SIZE_TYPE_1080_1024 = 'z' + const val SIZE_TYPE_2560_2048 = 'w' + } + @Ignore @IgnoredOnParcel private val sizesChars = Stack() init { - sizesChars.push('s') - sizesChars.push('m') - sizesChars.push('x') + sizesChars.push(SIZE_TYPE_75) + sizesChars.push(SIZE_TYPE_130) + sizesChars.push(SIZE_TYPE_604) sizesChars.push('o') sizesChars.push('p') sizesChars.push('q') sizesChars.push('r') - sizesChars.push('y') - sizesChars.push('z') - sizesChars.push('w') + sizesChars.push(SIZE_TYPE_807) + sizesChars.push(SIZE_TYPE_1080_1024) + sizesChars.push(SIZE_TYPE_2560_2048) } @IgnoredOnParcel @@ -61,7 +70,7 @@ data class VkPhoto( } fun getSizeOrSmaller(type: Char): BaseVkPhoto.Size? { - val photoStack = sizesChars.clone() as Stack + val photoStack = sizesChars.clone() as Stack<*> val sizeIndex = photoStack.search(type) @@ -72,7 +81,7 @@ data class VkPhoto( } for (i in 0 until photoStack.size) { - val size = getSizeOrNull(photoStack.peek()) + val size = getSizeOrNull(photoStack.peek() as Char) if (size == null) { photoStack.pop() 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 index 48a19402..55e968c6 100644 --- 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 @@ -7,5 +7,11 @@ data class VkStory( val id: Int, val ownerId: Int, val date: Int, - val photo: VkPhoto -) : VkAttachment() \ No newline at end of file + val photo: VkPhoto? +) : VkAttachment() { + + fun isFromUser() = ownerId > 0 + + fun isFromGroup() = ownerId < 0 + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVideo.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVideo.kt index b817897c..d70f29ba 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVideo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVideo.kt @@ -1,5 +1,6 @@ package com.meloda.fast.api.model.attachments +import android.os.Parcelable import com.meloda.fast.api.VkUtils import com.meloda.fast.api.model.base.attachments.BaseVkVideo import kotlinx.parcelize.IgnoredOnParcel @@ -9,7 +10,7 @@ import kotlinx.parcelize.Parcelize data class VkVideo( val id: Int, val ownerId: Int, - val images: List, + val images: List, val firstFrames: List?, val accessKey: String? ) : VkAttachment() { @@ -17,10 +18,57 @@ data class VkVideo( @IgnoredOnParcel val className: String = this::class.java.name - fun imageForWidth(width: Int): BaseVkVideo.Image? { + fun imageForWidth(width: Int): VideoImage? { return images.find { it.width == width } } + fun imageForWidthAtLeast(width: Int): VideoImage? { + var certainImages = images.sortedByDescending { it.width } + var containsVertical = false + for (image in images) { + if (image.shapeKind == ShapeKind.Vertical) { + containsVertical = true + break + } + } + + if (containsVertical) { + certainImages = certainImages.filter { it.shapeKind == ShapeKind.Vertical } + } + + certainImages = certainImages.filter { it.width >= width } + + return certainImages.firstOrNull() + } + + @Parcelize + data class VideoImage( + val width: Int, + val height: Int, + val url: String, + val withPadding: Boolean + ) : Parcelable { + + @IgnoredOnParcel + var shapeKind: ShapeKind + + init { + val ratio = width.toFloat() / height.toFloat() + + shapeKind = when { + ratio > 1 -> ShapeKind.Horizontal + ratio < 1 -> ShapeKind.Vertical + else -> ShapeKind.Square + } + } + } + + sealed class ShapeKind { + object Vertical : ShapeKind() + object Horizontal : ShapeKind() + object Square : ShapeKind() + } + override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString( attachmentClass = this::class.java, id = id, 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 index 6fcce196..4c72603b 100644 --- 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 @@ -12,8 +12,8 @@ data class VkVoiceMessage( val linkOgg: String, val linkMp3: String, val accessKey: String, - val transcriptState: String, - val transcript: String + val transcriptState: String?, + val transcript: String? ) : VkAttachment() { @IgnoredOnParcel 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 new file mode 100644 index 00000000..51949fd0 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkWidget.kt @@ -0,0 +1,8 @@ +package com.meloda.fast.api.model.attachments + +import kotlinx.parcelize.Parcelize + +@Parcelize +data class VkWidget( + val id: Int +) : VkAttachment() 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 index f431fbff..48d8bf61 100644 --- 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 @@ -8,17 +8,17 @@ import kotlinx.parcelize.Parcelize @Parcelize data class BaseVkMessage( + val id: Int, + val peer_id: Int, val date: Int, val from_id: Int, - val id: Int, val out: Int, - val peer_id: Int, val text: String, val conversation_message_id: Int, - val fwd_messages: List? = listOf(), + val fwd_messages: List? = emptyList(), val important: Boolean, val random_id: Int, - val attachments: List = listOf(), + val attachments: List = emptyList(), val is_hidden: Boolean, val payload: String, val geo: Geo?, @@ -29,7 +29,7 @@ data class BaseVkMessage( fun asVkMessage() = VkMessage( id = id, - text = if (text.isBlank()) null else text, + text = text.ifBlank { null }, isOut = out == 1, peerId = peer_id, fromId = from_id, 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 index cee8ee92..51f0138f 100644 --- 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 @@ -30,38 +30,40 @@ data class BaseVkAttachmentItem( val groupCall: BaseVkGroupCall?, val curator: BaseVkCurator?, val event: BaseVkEvent?, - val story: BaseVkStory? + 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"), - MINI_APP("mini_app"), - STICKER("sticker"), - GIFT("gift"), - WALL("wall"), - GRAFFITI("graffiti"), - POLL("poll"), - WALL_REPLY("wall_reply"), - CALL("call"), - GROUP_CALL_IN_PROGRESS("group_call_in_progress"), - CURATOR("curator"), - EVENT("event"), - STORY("story") + 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 + fun parse(value: String): AttachmentType { + val parsedValue = values().firstOrNull { it.value == value } ?: Unknown - if (parsedValue == UNKNOWN) { + if (parsedValue == Unknown) { Log.e("AttachmentType", "Unknown attachment type: $value") } 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 index d756c024..a1b09ce8 100644 --- 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 @@ -10,13 +10,11 @@ data class BaseVkEvent( val is_favorite: Boolean, val text: String, val address: String, - val friends: List = listOf(), + val friends: List = emptyList(), val member_status: Int, val time: Int ) : BaseVkAttachment() { - fun asVkEvent() = VkEvent( - id = id - ) + 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/BaseVkGroupCall.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGroupCall.kt index 41818361..e9ff17c0 100644 --- 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 @@ -1,6 +1,7 @@ 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 @@ -16,4 +17,6 @@ data class BaseVkGroupCall( val count: Int ) : Parcelable + fun asVkGroupCall() = VkGroupCall(initiatorId = initiator_id) + } 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 index 927f4404..8e857850 100644 --- 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 @@ -2,6 +2,7 @@ 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 @@ -63,4 +64,6 @@ data class BaseVkMiniApp( 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/BaseVkPoll.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPoll.kt index f5ec7d80..521145bb 100644 --- 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 @@ -1,6 +1,7 @@ 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 @@ -55,7 +56,8 @@ data class BaseVkPoll( 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/BaseVkStory.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkStory.kt index 846cf970..d16f2d51 100644 --- 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 @@ -17,7 +17,7 @@ data class BaseVkStory( val date: Int, val expires_at: Int, val is_ads: Boolean, - val photo: BaseVkPhoto, + val photo: BaseVkPhoto?, val replies: Replies, val is_one_time: Boolean, val track_code: String, @@ -40,7 +40,7 @@ data class BaseVkStory( id = id, ownerId = owner_id, date = date, - photo = photo.asVkPhoto() + photo = photo?.asVkPhoto() ) @Parcelize 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 index 258e7a97..15c3bcc4 100644 --- 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 @@ -41,18 +41,26 @@ data class BaseVkVideo( fun asVkVideo() = VkVideo( id = id, ownerId = owner_id, - images = image, + images = image.map { it.asVideoImage() }, firstFrames = first_frame, accessKey = access_key ) @Parcelize data class Image( - val height: Int, val width: Int, + val height: Int, val url: String, - val with_padding: Int - ) : Parcelable + val with_padding: Int? + ) : Parcelable { + + fun asVideoImage() = VkVideo.VideoImage( + width = width, + height = height, + url = url, + withPadding = with_padding == 1 + ) + } @Parcelize data class FirstFrame( 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 index 4445ffe0..23088e26 100644 --- 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 @@ -13,8 +13,8 @@ data class BaseVkVoiceMessage( val link_ogg: String, val link_mp3: String, val access_key: String, - val transcript_state: String, - val transcript: String + val transcript_state: String?, + val transcript: String? ) : Parcelable { fun asVkVoiceMessage() = VkVoiceMessage( diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWallReply.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWallReply.kt index 9abfb992..ab3e5a17 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWallReply.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWallReply.kt @@ -1,6 +1,7 @@ package com.meloda.fast.api.model.base.attachments import android.os.Parcelable +import com.meloda.fast.api.model.attachments.VkWallReply import kotlinx.parcelize.Parcelize @Parcelize @@ -17,7 +18,6 @@ data class BaseVkWallReply( val reply_to_comment: Int? ) : Parcelable { - @Parcelize data class Likes( val count: Int, @@ -26,4 +26,6 @@ data class BaseVkWallReply( val can_publish: Int ) : Parcelable + fun asVkWallReply() = VkWallReply(id = id) + } \ 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 new file mode 100644 index 00000000..a6bf1006 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkWidget.kt @@ -0,0 +1,11 @@ +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/network/AuthInterceptor.kt b/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt index b6f96691..1099a9cd 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt @@ -2,6 +2,7 @@ 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 okhttp3.Interceptor import okhttp3.Response import java.net.URLEncoder @@ -12,10 +13,12 @@ class AuthInterceptor : Interceptor { val builder = chain.request().url.newBuilder() .addQueryParameter("v", URLEncoder.encode(VKConstants.API_VERSION, "utf-8")) - UserConfig.accessToken.let { - if (it.isNotBlank()) - builder.addQueryParameter("access_token", URLEncoder.encode(it, "utf-8")) - } + + if (!builder.build().toUrl().toString().contains(AccountUrls.SetOnline)) + UserConfig.accessToken.let { + if (it.isNotBlank()) + builder.addQueryParameter("access_token", URLEncoder.encode(it, "utf-8")) + } // TODO: 9/29/2021 crash on timeout return chain.proceed(chain.request().newBuilder().apply { url(builder.build()) }.build()) 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 index 8799f165..dcbcb39c 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt @@ -5,6 +5,36 @@ object VkUrls { const val OAUTH = "https://oauth.vk.com" const val API = "https://api.vk.com/method" + object Auth { + const val DirectAuth = "$OAUTH/token" + const val SendSms = "$API/auth.validatePhone" + } + + object Conversations { + const val Get = "$API/messages.getConversations" + const val Delete = "$API/messages.deleteConversation" + const val Pin = "$API/messages.pinConversation" + const val Unpin = "$API/messages.unpinConversation" + const val ReorderPinned = "$API/messages.reorderPinnedConversations" + } + + object Users { + const val GetById = "$API/users.get" + } + + object Messages { + const val GetHistory = "$API/messages.getHistory" + const val Send = "$API/messages.send" + const val MarkAsImportant = "$API/messages.markAsImportant" + const val GetLongPollServer = "$API/messages.getLongPollServer" + const val GetLongPollHistory = "$API/messages.getLongPollHistory" + const val Pin = "$API/messages.pin" + const val Unpin = "$API/messages.unpin" + const val Delete = "$API/messages.delete" + const val Edit = "$API/messages.edit" + } + + } diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/account/AccountDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/account/AccountDataSource.kt new file mode 100644 index 00000000..6da02c69 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/account/AccountDataSource.kt @@ -0,0 +1,15 @@ +package com.meloda.fast.api.network.account + +import javax.inject.Inject + +class AccountDataSource @Inject constructor( + private val repo: AccountRepo +) { + + + suspend fun setOnline(params: AccountSetOnlineRequest) = repo.setOnline(params.map) + + suspend fun setOffline(params: AccountSetOfflineRequest) = repo.setOffline(params.map) + + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/account/AccountRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/account/AccountRepo.kt new file mode 100644 index 00000000..8abb02b7 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/account/AccountRepo.kt @@ -0,0 +1,17 @@ +package com.meloda.fast.api.network.account + +import com.meloda.fast.api.base.ApiResponse +import com.meloda.fast.api.network.Answer +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.QueryMap + +interface AccountRepo { + + @GET(AccountUrls.SetOnline) + suspend fun setOnline(@QueryMap params: Map): Answer> + + @POST(AccountUrls.SetOffline) + suspend fun setOffline(@QueryMap params: Map): Answer> + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/account/AccountRequests.kt b/app/src/main/kotlin/com/meloda/fast/api/network/account/AccountRequests.kt new file mode 100644 index 00000000..187f7002 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/account/AccountRequests.kt @@ -0,0 +1,20 @@ +package com.meloda.fast.api.network.account + +import com.meloda.fast.api.ApiExtensions.intString + +data class AccountSetOnlineRequest( + val voip: Boolean, + val accessToken: String +) { + + val map + get() = mutableMapOf( + "voip" to voip.intString, + "access_token" to accessToken + ) + +} + +data class AccountSetOfflineRequest(val accessToken: String) { + val map get() = mutableMapOf("access_token" to accessToken) +} 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 new file mode 100644 index 00000000..c3588a2c --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/account/AccountUrls.kt @@ -0,0 +1,10 @@ +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/auth/AuthDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthDataSource.kt index 49e9daaa..d17da88e 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthDataSource.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthDataSource.kt @@ -6,7 +6,7 @@ class AuthDataSource @Inject constructor( private val repo: AuthRepo ) { - suspend fun auth(params: RequestAuthDirect) = repo.auth(params.map) + suspend fun auth(params: AuthDirectRequest) = repo.auth(params.map) suspend fun sendSms(validationSid: String) = repo.sendSms(validationSid) diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRepo.kt index 0ae20561..320a9d04 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRepo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRepo.kt @@ -8,9 +8,9 @@ import retrofit2.http.QueryMap interface AuthRepo { @GET(AuthUrls.DirectAuth) - suspend fun auth(@QueryMap param: Map): Answer + suspend fun auth(@QueryMap param: Map): Answer @GET(AuthUrls.SendSms) - suspend fun sendSms(@Query("sid") validationSid: String): Answer + suspend fun sendSms(@Query("sid") validationSid: String): Answer } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRequest.kt b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRequest.kt index 2226ba1b..98e8fdbd 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRequest.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRequest.kt @@ -1,23 +1,25 @@ package com.meloda.fast.api.network.auth import android.os.Parcelable -import com.google.gson.annotations.SerializedName +import com.meloda.fast.BuildConfig +import com.meloda.fast.api.VKConstants import kotlinx.parcelize.Parcelize @Parcelize -data class RequestAuthDirect( - @SerializedName("grant_type") val grantType: String, - @SerializedName("client_id") val clientId: String, - @SerializedName("client_secret") val clientSecret: String, - @SerializedName("username") val username: String, - @SerializedName("password") val password: String, - @SerializedName("scope") val scope: String, - @SerializedName("2fa_supported") val twoFaSupported: Boolean = true, - @SerializedName("force_sms") val twoFaForceSms: Boolean = false, - @SerializedName("code") val twoFaCode: String? = null, - @SerializedName("captcha_sid") val captchaSid: String? = null, - @SerializedName("captcha_key") val captchaKey: String? = null, +data class AuthDirectRequest( + val grantType: String, + val clientId: String, + val clientSecret: String, + val username: String, + val password: String, + val scope: String, + val twoFaSupported: Boolean = true, + val twoFaForceSms: Boolean = false, + val twoFaCode: String? = null, + val captchaSid: String? = null, + val captchaKey: String? = null, ) : Parcelable { + val map get() = mutableMapOf( "grant_type" to grantType, @@ -28,10 +30,38 @@ data class RequestAuthDirect( "scope" to scope, "2fa_supported" to if (twoFaSupported) "1" else "0", "force_sms" to if (twoFaForceSms) "1" else "0" - ) + ) .apply { twoFaCode?.let { this["code"] = it } captchaSid?.let { this["captcha_sid"] = it } captchaKey?.let { this["captcha_key"] = it } } +} + +@Parcelize +data class AuthWithAppRequest( + val redirectUrl: String = "https://oauth.vk.com/blank.html", + val display: String = "page", + val responseType: String = "token", + val accessToken: String, + val revoke: Int = 1, + val scope: Int = 136297695, + val clientId: String = VKConstants.FAST_APP_ID, + val sdkPackage: String = BuildConfig.sdkPackage, + val sdkFingerprint: String = BuildConfig.sdkFingerprint +) : Parcelable { + + val map + get() = mutableMapOf( + "redirect_url" to redirectUrl, + "display" to display, + "response_type" to responseType, + "access_token" to accessToken, + "revoke" to revoke.toString(), + "scope" to scope.toString(), + "client_id" to clientId, + "sdk_package" to sdkPackage, + "sdk_fingerprint" to sdkFingerprint + ) + } \ 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 index 2da17dee..6e66db70 100644 --- 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 @@ -5,7 +5,7 @@ import com.google.gson.annotations.SerializedName import kotlinx.parcelize.Parcelize @Parcelize -data class ResponseAuthDirect( +data class AuthDirectResponse( @SerializedName("access_token") val accessToken: String? = null, @SerializedName("user_id") val userId: Int? = null, @SerializedName("trusted_hash") val twoFaHash: String? = null, @@ -13,7 +13,7 @@ data class ResponseAuthDirect( ) : Parcelable @Parcelize -data class ResponseSendSms( +data class SendSmsResponse( @SerializedName("sid") val validationSid: String?, @SerializedName("delay") val delay: Int?, @SerializedName("validation_type") val validationType: String?, diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/longpoll/LongPollRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/longpoll/LongPollRepo.kt index 997626d8..14416913 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/longpoll/LongPollRepo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/longpoll/LongPollRepo.kt @@ -1,18 +1,17 @@ package com.meloda.fast.api.network.longpoll -import com.meloda.fast.api.base.ApiResponse +import com.google.gson.JsonObject import com.meloda.fast.api.network.Answer -import org.json.JSONObject import retrofit2.http.GET -import retrofit2.http.Path import retrofit2.http.QueryMap +import retrofit2.http.Url interface LongPollRepo { - @GET("https://{serverUrl}") + @GET suspend fun getResponse( - @Path("serverUrl") serverUrl: String, + @Url serverUrl: String, @QueryMap params: Map - ): Answer> + ): Answer } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/longpoll/LongPollRequests.kt b/app/src/main/kotlin/com/meloda/fast/api/network/longpoll/LongPollRequests.kt new file mode 100644 index 00000000..d44cfe91 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/longpoll/LongPollRequests.kt @@ -0,0 +1,24 @@ +package com.meloda.fast.api.network.longpoll + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class LongPollGetUpdatesRequest( + val act: String = "a_check", + val key: String, + val ts: Int, + val wait: Int, + val mode: Int +) : Parcelable { + + val map + get() = mutableMapOf( + "act" to act, + "key" to key, + "ts" to ts.toString(), + "wait" to wait.toString(), + "mode" to mode.toString() + ) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesDataSource.kt index 1fee173b..15955175 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesDataSource.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesDataSource.kt @@ -1,40 +1,50 @@ package com.meloda.fast.api.network.messages import com.meloda.fast.api.model.VkMessage +import com.meloda.fast.api.network.longpoll.LongPollGetUpdatesRequest +import com.meloda.fast.api.network.longpoll.LongPollRepo import com.meloda.fast.database.dao.MessagesDao import javax.inject.Inject class MessagesDataSource @Inject constructor( - private val repo: MessagesRepo, - private val dao: MessagesDao + private val messagesRepo: MessagesRepo, + private val messagesDao: MessagesDao, + private val longPollRepo: LongPollRepo ) { + suspend fun store(messages: List) = messagesDao.insert(messages) + + suspend fun getCached(peerId: Int) = messagesDao.getByPeerId(peerId) + suspend fun getHistory(params: MessagesGetHistoryRequest) = - repo.getHistory(params.map) + messagesRepo.getHistory(params.map) suspend fun send(params: MessagesSendRequest) = - repo.send(params.map) + messagesRepo.send(params.map) suspend fun markAsImportant(params: MessagesMarkAsImportantRequest) = - repo.markAsImportant(params.map) + messagesRepo.markAsImportant(params.map) suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) = - repo.getLongPollServer(params.map) + messagesRepo.getLongPollServer(params.map) suspend fun pin(params: MessagesPinMessageRequest) = - repo.pin(params.map) + messagesRepo.pin(params.map) suspend fun unpin(params: MessagesUnPinMessageRequest) = - repo.unpin(params.map) + messagesRepo.unpin(params.map) suspend fun delete(params: MessagesDeleteRequest) = - repo.delete(params.map) + messagesRepo.delete(params.map) suspend fun edit(params: MessagesEditRequest) = - repo.edit(params.map) + messagesRepo.edit(params.map) - suspend fun store(messages: List) = dao.insert(messages) - - suspend fun getCached(peerId: Int) = dao.getByPeerId(peerId) + suspend fun getLongPollUpdates( + serverUrl: String, + params: LongPollGetUpdatesRequest + ) = longPollRepo.getResponse(serverUrl, params.map) + suspend fun getById(params: MessagesGetByIdRequest) = + messagesRepo.getById(params.map) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRepo.kt index c522339e..3d61be52 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRepo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRepo.kt @@ -42,4 +42,8 @@ interface MessagesRepo { @POST(MessagesUrls.Edit) suspend fun edit(@FieldMap params: Map): Answer> + @FormUrlEncoded + @POST(MessagesUrls.GetById) + suspend fun getById(@FieldMap params: Map): Answer> + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRequest.kt b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRequest.kt index ab37b587..4801203c 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRequest.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRequest.kt @@ -40,7 +40,8 @@ data class MessagesSendRequest( val replyTo: Int? = null, val stickerId: Int? = null, val disableMentions: Boolean? = null, - val dontParseLinks: Boolean? = null + val dontParseLinks: Boolean? = null, + val silent: Boolean? = null ) : Parcelable { val map @@ -55,6 +56,7 @@ data class MessagesSendRequest( stickerId?.let { this["sticker_id"] = it.toString() } disableMentions?.let { this["disable_mentions"] = it.intString } dontParseLinks?.let { this["dont_parse_links"] = it.intString } + silent?.let { this["silent"] = it.toString() } } } @@ -165,4 +167,21 @@ data class MessagesEditRequest( } } +} + +@Parcelize +data class MessagesGetByIdRequest( + val messagesIds: List, + val extended: Boolean? = null, + val fields: String? = null +) : Parcelable { + + val map + get() = mutableMapOf( + "message_ids" to messagesIds.joinToString { it.toString() }, + ).apply { + extended?.let { this["extended"] = it.intString } + fields?.let { this["fields"] = it } + } + } \ 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 index 1462c031..b881d145 100644 --- 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 @@ -10,8 +10,16 @@ import kotlinx.parcelize.Parcelize @Parcelize data class MessagesGetHistoryResponse( val count: Int, - val items: List = listOf(), + 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 \ No newline at end of file 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 index 5910cfed..bb75b0eb 100644 --- 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 @@ -13,5 +13,6 @@ object MessagesUrls { 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" } \ 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 index a3e6804e..dba3f764 100644 --- a/app/src/main/kotlin/com/meloda/fast/base/BaseActivity.kt +++ b/app/src/main/kotlin/com/meloda/fast/base/BaseActivity.kt @@ -1,40 +1,12 @@ package com.meloda.fast.base -import android.os.Bundle import androidx.annotation.LayoutRes import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LifecycleRegistry -abstract class BaseActivity : AppCompatActivity, LifecycleOwner { +abstract class BaseActivity : AppCompatActivity { constructor() : super() constructor(@LayoutRes resId: Int) : super(resId) - protected lateinit var lifecycleRegistry: LifecycleRegistry - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - lifecycleRegistry = LifecycleRegistry(this) - lifecycleRegistry.currentState = Lifecycle.State.CREATED - } - - override fun onStart() { - super.onStart() - lifecycleRegistry.currentState = Lifecycle.State.STARTED - } - - override fun onResume() { - super.onResume() - lifecycleRegistry.currentState = Lifecycle.State.RESUMED - } - - override fun onDestroy() { - super.onDestroy() - lifecycleRegistry.currentState = Lifecycle.State.DESTROYED - } - } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/base/ResourceManager.kt b/app/src/main/kotlin/com/meloda/fast/base/ResourceManager.kt new file mode 100644 index 00000000..f0dcf582 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/base/ResourceManager.kt @@ -0,0 +1,20 @@ +package com.meloda.fast.base + +import android.content.Context +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.StringRes +import androidx.core.content.ContextCompat + +abstract class ResourceManager(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) + } + +} \ No newline at end of file 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 index 74870bc6..4455cb34 100644 --- a/app/src/main/kotlin/com/meloda/fast/base/adapter/BaseAdapter.kt +++ b/app/src/main/kotlin/com/meloda/fast/base/adapter/BaseAdapter.kt @@ -1,115 +1,187 @@ package com.meloda.fast.base.adapter +import android.annotation.SuppressLint import android.content.Context import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import android.widget.AdapterView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter +import com.meloda.fast.model.DataItem +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext -@Suppress("MemberVisibilityCanBePrivate", "unused") -abstract class BaseAdapter( +@Suppress("MemberVisibilityCanBePrivate", "unused", "UNCHECKED_CAST") +@SuppressLint("NotifyDataSetChanged") +abstract class BaseAdapter, VH : BaseHolder> constructor( var context: Context, - values: MutableList, - diffUtil: DiffUtil.ItemCallback -) : ListAdapter(diffUtil) { + diffUtil: DiffUtil.ItemCallback, + preAddedValues: List = emptyList(), +) : ListAdapter(diffUtil) { - val cleanValues = mutableListOf() - val values = mutableListOf() - - init { - addAll(values) - } + protected val adapterScope = CoroutineScope(Dispatchers.Default) + private val cleanList = mutableListOf() protected var inflater: LayoutInflater = LayoutInflater.from(context) - var itemClickListener: ((position: Int) -> Unit) = {} - var itemLongClickListener: ((position: Int) -> Boolean) = { false } + var itemClickListener: ((position: Int) -> Unit)? = null + var itemLongClickListener: ((position: Int) -> Boolean)? = null + + init { + cleanList.addAll(preAddedValues) + addAll(preAddedValues) + } + + fun cloneCurrentList(): MutableList { + return ArrayList(currentList) + } open fun destroy() {} - override fun getItem(position: Int): Item { - return values[position] + fun getOrNull(position: Int): T? { + return if (position >= 0 && position <= currentList.lastIndex) get(position) else null } - fun getOrNull(position: Int): Item? { - return if (position >= 0 && position <= values.lastIndex) get(position) else null - } - - fun getOrElse(position: Int, defaultValue: (Int) -> Item): Item { - return if (position >= 0 && position <= values.lastIndex) get(position) + fun getOrElse(position: Int, defaultValue: (Int) -> T): T { + return if (position >= 0 && position <= currentList.lastIndex) get(position) else defaultValue(position) } - fun add(position: Int, item: Item) { - values.add(position, item) - cleanValues.add(position, item) + fun add( + item: T, + position: Int? = null, + beforeFooter: Boolean = false, + commitCallback: (() -> Unit)? = null + ) = addAll(listOf(item), position, beforeFooter, commitCallback) + + fun addAll( + items: List, + position: Int? = null, + beforeFooter: Boolean = false, + commitCallback: (() -> Unit)? = null + ) { + adapterScope.launch { + val newList = cloneCurrentList() + if (position == null) { + val mutableItems = items.toMutableList() + if (beforeFooter && newList.lastOrNull() is DataItem.Footer) { + newList.removeLastOrNull() + } + + if (beforeFooter) { + mutableItems += DataItem.Footer as T + } + + newList.addAll(mutableItems) + cleanList.addAll(mutableItems) + } else { + newList.addAll(position, items) + cleanList.addAll(position, items) + } + + withContext(Dispatchers.Main) { + submitList(newList, commitCallback) + } + } } - fun add(item: Item) { - values += item - cleanValues.add(item) + fun remove(item: T, commitCallback: (() -> Unit)? = null) = + removeAll(listOf(item), commitCallback) + + fun removeAll(items: List, commitCallback: (() -> Unit)? = null) { + val newList = cloneCurrentList() + newList.removeAll(items) + submitList(newList, commitCallback) + + cleanList.removeAll(items) } - fun addAll(items: List) { - values += items - cleanValues.addAll(items) + fun removeAt(index: Int, commitCallback: (() -> Unit)? = null) { + val newList = cloneCurrentList() + newList.removeAt(index) + submitList(newList, commitCallback) + + cleanList.removeAt(index) } - fun addAll(position: Int, items: List) { - values.addAll(position, items) - cleanValues.addAll(position, items) + fun clear(commitCallback: (() -> Unit)? = null) = removeAll(currentList, commitCallback) + + fun setItem( + item: T, + withHeader: Boolean = false, + withFooter: Boolean = false, + commitCallback: (() -> Unit)? = null + ) = setItems(listOf(item), withHeader, withFooter, commitCallback) + + @Suppress("UNCHECKED_CAST") + fun setItems( + list: List?, + withHeader: Boolean = false, + withFooter: Boolean = false, + commitCallback: (() -> Unit)? = null + ) { + adapterScope.launch { + val items = mutableListOf() + if (withHeader) items.add(DataItem.Header as T) + if (!list.isNullOrEmpty()) items.addAll(list) + if (withFooter) items.add(DataItem.Footer as T) + + withContext(Dispatchers.Main) { + if (items == currentList) { + refreshList() + } else { + submitList(items, commitCallback) + } + } + } } - fun removeAll(items: List) { - values.removeAll(items) - cleanValues.removeAll(items) + fun indexOf(item: T): Int { + return currentList.indexOf(item) } - fun removeAt(index: Int) { - values.removeAt(index) - cleanValues.removeAt(index) + val indices get() = currentList.indices + + operator fun get(position: Int): T { + return currentList[position] } - fun remove(item: Item) { - values.remove(item) - cleanValues.remove(item) + 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 + submitList(newList, commitCallback) + + cleanList[position] = item } - fun clear() { - values.clear() - cleanValues.clear() + fun isEmpty() = currentList.isEmpty() + fun isNotEmpty() = currentList.isNotEmpty() + + @SuppressLint("NotifyDataSetChanged") + fun refreshList() { + notifyDataSetChanged() } - operator fun get(position: Int): Item { - return values[position] + fun updateCleanList(list: List?) { + cleanList.clear() + list?.run { cleanList.addAll(this) } } - operator fun set(position: Int, item: Item) { - values[position] = item - cleanValues[position] = item + override fun submitList(list: List?) { + super.submitList(list) + updateCleanList(list) } - open fun notifyChanges(oldList: List, newList: List) {} - - fun isEmpty() = values.isEmpty() - fun isNotEmpty() = values.isNotEmpty() - - fun view(resId: Int, viewGroup: ViewGroup, attachToRoot: Boolean = false): View { - return inflater.inflate(resId, viewGroup, attachToRoot) - } - - fun updateValues(list: MutableList) { - values.clear() - values += list + override fun submitList(list: List?, commitCallback: Runnable?) { + super.submitList(list, commitCallback) + updateCleanList(list) } override fun onBindViewHolder(holder: VH, position: Int) { - onBindItemViewHolder(holder, position) - } - - private fun onBindItemViewHolder(holder: VH, position: Int) { initListeners(holder.itemView, position) holder.bind(position) } @@ -117,15 +189,16 @@ abstract class BaseAdapter( protected open fun initListeners(itemView: View, position: Int) { if (itemView is AdapterView<*>) return - itemView.setOnClickListener { itemClickListener.invoke(position) } - itemView.setOnLongClickListener { itemLongClickListener.invoke(position) } + itemView.setOnClickListener { itemClickListener?.invoke(position) } + itemView.setOnLongClickListener { + itemLongClickListener?.invoke(position) + return@setOnLongClickListener itemClickListener != null + } } override fun getItemCount(): Int { - return values.size + return currentList.size } - val lastPosition - get() = itemCount - 1 - -} + val lastPosition get() = currentList.lastIndex +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/base/adapter/EmptyHeaderAdapter.kt b/app/src/main/kotlin/com/meloda/fast/base/adapter/EmptyHeaderAdapter.kt index 6da60751..7fa4b4ac 100644 --- a/app/src/main/kotlin/com/meloda/fast/base/adapter/EmptyHeaderAdapter.kt +++ b/app/src/main/kotlin/com/meloda/fast/base/adapter/EmptyHeaderAdapter.kt @@ -5,6 +5,7 @@ import android.view.View import android.view.ViewGroup import androidx.core.view.isInvisible import androidx.recyclerview.widget.RecyclerView +import com.meloda.fast.extensions.dpToPx import com.meloda.fast.util.AndroidUtils import kotlin.math.roundToInt @@ -24,7 +25,7 @@ class EmptyHeaderAdapter( private fun generateHeaderView() = View(context).apply { layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, - AndroidUtils.px(56).roundToInt() + 56.dpToPx() ) isClickable = false isEnabled = false diff --git a/app/src/main/kotlin/com/meloda/fast/base/adapter/Items.kt b/app/src/main/kotlin/com/meloda/fast/base/adapter/Items.kt deleted file mode 100644 index cfa64546..00000000 --- a/app/src/main/kotlin/com/meloda/fast/base/adapter/Items.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.meloda.fast.base.adapter - -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/base/viewmodel/Events.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/Events.kt index 52f55dfa..71b2bad2 100644 --- a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/Events.kt +++ b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/Events.kt @@ -16,4 +16,8 @@ data class ValidationEvent(val sid: String) : VkEvent() object StartProgressEvent : VkEvent() object StopProgressEvent : VkEvent() -abstract class VkEvent \ No newline at end of file +abstract class VkEvent + +interface VkEventCallback { + fun onEvent(event: T) +} \ 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 index 279cc944..e334c772 100644 --- a/app/src/main/kotlin/com/meloda/fast/common/AppGlobal.kt +++ b/app/src/main/kotlin/com/meloda/fast/common/AppGlobal.kt @@ -30,7 +30,7 @@ class AppGlobal : Application() { lateinit var preferences: SharedPreferences lateinit var resources: Resources lateinit var packageName: String - lateinit var instance: AppGlobal + private lateinit var instance: AppGlobal lateinit var appDatabase: AppDatabase @@ -41,6 +41,8 @@ class AppGlobal : Application() { var screenWidth = 0 var screenHeight = 0 + + val Instance get() = instance } override fun onCreate() { @@ -51,9 +53,7 @@ class AppGlobal : Application() { ACRA.init(this) } - appDatabase = Room.databaseBuilder( - this, AppDatabase::class.java, "cache" - ) + appDatabase = Room.databaseBuilder(this, AppDatabase::class.java, "cache") .fallbackToDestructiveMigration() .build() @@ -85,10 +85,8 @@ class AppGlobal : Application() { "width: $screenWidth; height: $screenHeight; density: $density; diagonal: $diagonal; dpiDensity: $densityDpi; scaledDensity: $densityScaled; xDpi: $xDpi; yDpi: $yDpi" ) - inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager } - } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/common/Screens.kt b/app/src/main/kotlin/com/meloda/fast/common/Screens.kt new file mode 100644 index 00000000..3060bb55 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/common/Screens.kt @@ -0,0 +1,17 @@ +package com.meloda.fast.common + +import android.os.Bundle +import com.github.terrakok.cicerone.androidx.FragmentScreen +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.MessagesHistoryFragment + +@Suppress("FunctionName") +object Screens { + fun Main() = FragmentScreen { MainFragment() } + fun Login() = FragmentScreen { LoginFragment() } + fun Conversations() = FragmentScreen { ConversationsFragment() } + fun MessagesHistory(bundle: Bundle) = + FragmentScreen { MessagesHistoryFragment.newInstance(bundle) } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/database/AppDatabase.kt b/app/src/main/kotlin/com/meloda/fast/database/AppDatabase.kt index e8d5c859..d3db872b 100644 --- a/app/src/main/kotlin/com/meloda/fast/database/AppDatabase.kt +++ b/app/src/main/kotlin/com/meloda/fast/database/AppDatabase.kt @@ -19,7 +19,7 @@ import com.meloda.fast.database.dao.UsersDao VkUser::class, VkGroup::class ], - version = 26, + version = 28, exportSchema = false, ) @TypeConverters(Converters::class) diff --git a/app/src/main/kotlin/com/meloda/fast/database/Converters.kt b/app/src/main/kotlin/com/meloda/fast/database/Converters.kt index e066617a..400d3633 100644 --- a/app/src/main/kotlin/com/meloda/fast/database/Converters.kt +++ b/app/src/main/kotlin/com/meloda/fast/database/Converters.kt @@ -5,17 +5,19 @@ import com.google.gson.Gson import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.attachments.VkAttachment import org.json.JSONObject -import java.util.stream.Collectors +@Suppress("UnnecessaryVariable") class Converters { + private companion object { + private const val CACHE_SEPARATOR = "fastkruta228355" + } + @TypeConverter fun fromListVkMessageToString(messages: List?): String? { if (messages == null) return null - val string = - messages.map { fromVkMessageToString(it)!! }.stream() - .collect(Collectors.joining("fastkruta228355")) + val string = messages.map { fromVkMessageToString(it)!! }.joinToString { CACHE_SEPARATOR } return string } @@ -24,9 +26,9 @@ class Converters { fun fromStringToListVkMessage(string: String?): List? { if (string == null) return null - if (string.contains("fastkruta228355")) { + if (string.contains(CACHE_SEPARATOR)) { val messages = - string.split("fastkruta228355").map { fromStringToVkMessage(it)!! } + string.split(CACHE_SEPARATOR).map { fromStringToVkMessage(it)!! } return messages } @@ -55,8 +57,7 @@ class Converters { if (attachments == null) return null val string = - attachments.map { fromVkAttachmentToString(it)!! }.stream() - .collect(Collectors.joining("fastkruta228355")) + attachments.map { fromVkAttachmentToString(it)!! }.joinToString { CACHE_SEPARATOR } return string } @@ -65,9 +66,9 @@ class Converters { fun fromStringToListVkAttachment(string: String?): List? { if (string == null) return null - if (string.contains("fastkruta228355")) { + if (string.contains(CACHE_SEPARATOR)) { val attachments = - string.split("fastkruta228355").map { fromStringToVkAttachment(it)!! } + string.split(CACHE_SEPARATOR).map { fromStringToVkAttachment(it)!! } return attachments } diff --git a/app/src/main/kotlin/com/meloda/fast/di/NavigationModule.kt b/app/src/main/kotlin/com/meloda/fast/di/NavigationModule.kt new file mode 100644 index 00000000..a996124d --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/di/NavigationModule.kt @@ -0,0 +1,25 @@ +package com.meloda.fast.di + +import com.github.terrakok.cicerone.Cicerone +import com.github.terrakok.cicerone.Router +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +object NavigationModule { + @Provides + @Singleton + fun getCicerone(): Cicerone = Cicerone.create() + + @Provides + @Singleton + fun getRouter(cicerone: Cicerone) = cicerone.router + + @Provides + @Singleton + fun getNavigationHolder(cicerone: Cicerone) = cicerone.getNavigatorHolder() +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt b/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt index 989d3402..f94afeb4 100644 --- a/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt +++ b/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt @@ -2,16 +2,19 @@ package com.meloda.fast.di import com.google.gson.Gson import com.google.gson.GsonBuilder +import com.meloda.fast.api.LongPollUpdatesParser import com.meloda.fast.api.network.AuthInterceptor import com.meloda.fast.api.network.ResultCallFactory -import com.meloda.fast.api.network.auth.AuthRepo +import com.meloda.fast.api.network.account.AccountDataSource +import com.meloda.fast.api.network.account.AccountRepo import com.meloda.fast.api.network.auth.AuthDataSource +import com.meloda.fast.api.network.auth.AuthRepo import com.meloda.fast.api.network.conversations.ConversationsDataSource import com.meloda.fast.api.network.conversations.ConversationsRepo import com.meloda.fast.api.network.longpoll.LongPollRepo import com.meloda.fast.api.network.messages.MessagesDataSource -import com.meloda.fast.api.network.users.UsersDataSource import com.meloda.fast.api.network.messages.MessagesRepo +import com.meloda.fast.api.network.users.UsersDataSource import com.meloda.fast.api.network.users.UsersRepo import com.meloda.fast.database.dao.ConversationsDao import com.meloda.fast.database.dao.MessagesDao @@ -67,22 +70,27 @@ object NetworkModule { fun provideAuthInterceptor(): AuthInterceptor = AuthInterceptor() @Provides + @Singleton fun provideAuthRepo(retrofit: Retrofit): AuthRepo = retrofit.create(AuthRepo::class.java) @Provides + @Singleton fun provideConversationsRepo(retrofit: Retrofit): ConversationsRepo = retrofit.create(ConversationsRepo::class.java) @Provides + @Singleton fun provideUsersRepo(retrofit: Retrofit): UsersRepo = retrofit.create(UsersRepo::class.java) @Provides + @Singleton fun provideMessagesRepo(retrofit: Retrofit): MessagesRepo = retrofit.create(MessagesRepo::class.java) @Provides + @Singleton fun provideLongPollRepo(retrofit: Retrofit): LongPollRepo = retrofit.create(LongPollRepo::class.java) @@ -109,7 +117,27 @@ object NetworkModule { @Provides @Singleton fun provideMessagesDataSource( - repo: MessagesRepo, - dao: MessagesDao - ): MessagesDataSource = MessagesDataSource(repo, dao) + messagesRepo: MessagesRepo, + messagesDao: MessagesDao, + longPollRepo: LongPollRepo + ): MessagesDataSource = MessagesDataSource( + messagesRepo = messagesRepo, + messagesDao = messagesDao, + longPollRepo = longPollRepo + ) + + @Provides + @Singleton + fun provideLongPollUpdatesParser(messagesDataSource: MessagesDataSource): LongPollUpdatesParser = + LongPollUpdatesParser(messagesDataSource) + + @Provides + @Singleton + fun provideAccountRepo(retrofit: Retrofit): AccountRepo = + retrofit.create(AccountRepo::class.java) + + @Provides + @Singleton + fun provideAccountDataSource(repo: AccountRepo): AccountDataSource = + AccountDataSource(repo) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/extensions/Ext.kt b/app/src/main/kotlin/com/meloda/fast/extensions/Ext.kt new file mode 100644 index 00000000..3a5ebdbc --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/extensions/Ext.kt @@ -0,0 +1,87 @@ +package com.meloda.fast.extensions + +import android.animation.ValueAnimator +import android.content.res.Resources +import android.os.Build +import android.os.Parcelable +import android.util.DisplayMetrics +import android.util.SparseArray +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.Px +import androidx.annotation.StyleRes +import androidx.core.view.children + +fun Int.dpToPx(): Int { + val metrics = Resources.getSystem().displayMetrics + return (this * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt() +} + +fun Float.dpToPx(): Int { + val metrics = Resources.getSystem().displayMetrics + return (this * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt() +} + +fun TextView.clear() { + text = null +} + +fun ViewGroup.saveChildViewStates(): SparseArray { + val childViewStates = SparseArray() + children.forEach { child -> child.saveHierarchyState(childViewStates) } + return childViewStates +} + +fun ViewGroup.restoreChildViewStates(childViewStates: SparseArray) { + children.forEach { child -> child.restoreHierarchyState(childViewStates) } +} + +fun View.invisible() = run { visibility = View.INVISIBLE } + +fun View.visible() = run { visibility = View.VISIBLE } +fun View.gone() = run { visibility = View.GONE } + +@JvmOverloads +fun View.toggleVisibility(visible: Boolean?, visibilityWhenFalse: Int = View.GONE) = + run { visibility = if (visible == true) View.VISIBLE else visibilityWhenFalse } + +fun ValueAnimator.startWithIntValues(from: Int, to: Int) { + setIntValues(from, to) + start() +} + +fun View.setMarginsPx( + @Px leftMargin: Int? = null, + @Px topMargin: Int? = null, + @Px rightMargin: Int? = null, + @Px bottomMargin: Int? = null +) { + if (layoutParams is ViewGroup.MarginLayoutParams) { + val params = layoutParams as ViewGroup.MarginLayoutParams + leftMargin?.run { params.leftMargin = this } + topMargin?.run { params.topMargin = this } + rightMargin?.run { params.rightMargin = this } + bottomMargin?.run { params.bottomMargin = this } + requestLayout() + } +} + +inline fun Pair.runIfElementsNotNull(block: (T, K) -> Unit) { + val firstCopy = first + val secondCopy = second + if (firstCopy != null && secondCopy != null) { + block(firstCopy, secondCopy) + } +} + +@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 +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/extensions/GlideExt.kt b/app/src/main/kotlin/com/meloda/fast/extensions/GlideExt.kt new file mode 100644 index 00000000..5ad156f9 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/extensions/GlideExt.kt @@ -0,0 +1,139 @@ +package com.meloda.fast.extensions + +import android.graphics.Bitmap +import android.graphics.Color +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( + url: String? = null, + uri: Uri? = null, + drawableRes: Int? = null, + drawable: Drawable? = null, + placeholderDrawable: Drawable = ColorDrawable(Color.TRANSPARENT), + errorDrawable: Drawable = placeholderDrawable, + crossFade: Boolean = false, + crossFadeDuration: Int = 200, + transformations: List = emptyList(), + onLoadedAction: (() -> Unit)? = null, + onFailedAction: (() -> Unit)? = null, + priority: Priority = Priority.NORMAL, + cacheStrategy: DiskCacheStrategy = DiskCacheStrategy.ALL + ) { + val request = Glide.with(this) + + var builder = when { + url != null -> request.load(url) + uri != null -> request.load(uri) + drawableRes != null -> request.load(drawableRes) + drawable != null -> request.load(drawable) + else -> request.load(null as Drawable?) + } + + builder = builder + .apply(TypeTransformations.createRequestOptions(transformations)) + .error(errorDrawable) + .placeholder(placeholderDrawable) + .addListener(ImageLoadRequestListener(onLoadedAction, onFailedAction)) + .diskCacheStrategy(cacheStrategy) + .priority(priority) + + if (crossFade) { + builder = builder.transition(withCrossFade(crossFadeDuration)) + } + + builder.into(this) + } +} + +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 + } +} + +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) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/extensions/NavigationExtensions.kt b/app/src/main/kotlin/com/meloda/fast/extensions/NavigationExtensions.kt deleted file mode 100644 index b1313de3..00000000 --- a/app/src/main/kotlin/com/meloda/fast/extensions/NavigationExtensions.kt +++ /dev/null @@ -1,266 +0,0 @@ -package com.meloda.fast.extensions - -import android.content.Intent -import androidx.collection.SparseArrayCompat -import androidx.collection.forEach -import androidx.collection.set -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.navigation.NavController -import androidx.navigation.fragment.NavHostFragment -import com.google.android.material.bottomnavigation.BottomNavigationView -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig - -/** - * Manages the various graphs needed for a [BottomNavigationView]. - * - * This sample is a workaround until the Navigation Component supports multiple back stacks. - */ -object NavigationExtensions { - - fun BottomNavigationView.setupWithNavController( - navGraphIds: List, - fragmentManager: FragmentManager, - containerId: Int, - intent: Intent - ): LiveData { - - // Map of tags - val graphIdToTagMap = SparseArrayCompat() - // Result. Mutable live data with the selected controlled - val selectedNavController = MutableLiveData() - - var firstFragmentGraphId = 0 - - // First create a NavHostFragment for each NavGraph ID - navGraphIds.forEachIndexed { index, navGraphId -> - val fragmentTag = getFragmentTag(index) - - // Find or create the Navigation host fragment - val navHostFragment = obtainNavHostFragment( - fragmentManager, - fragmentTag, - navGraphId, - containerId - ) - - // Obtain its id - val graphId = navHostFragment.navController.graph.id - - if (index == 0) { - firstFragmentGraphId = graphId - } - - // Save to the map - graphIdToTagMap[graphId] = fragmentTag - - // Attach or detach nav host fragment depending on whether it's the selected item. - if (this.selectedItemId == graphId) { - // Update livedata with the selected graph - selectedNavController.value = navHostFragment.navController - attachNavHostFragment(fragmentManager, navHostFragment, index == 0) - } else { - detachNavHostFragment(fragmentManager, navHostFragment) - } - } - - // Now connect selecting an item with swapping Fragments - var selectedItemTag = graphIdToTagMap[this.selectedItemId] - val firstFragmentTag = graphIdToTagMap[firstFragmentGraphId] ?: "" - var isOnFirstFragment = selectedItemTag == firstFragmentTag - - setOnItemSelectedListener { item -> - // Don't do anything if the state is state has already been saved. - if (fragmentManager.isStateSaved) { - false - } else { - val navController = - (fragmentManager.findFragmentByTag(selectedItemTag) as NavHostFragment).navController - navController.popBackStack(navController.graph.startDestination, false) - if (selectedItemTag != graphIdToTagMap[item.itemId]) { - val newlySelectedItemTag = //graphIdToTagMap[item.itemId] - if (!UserConfig.isLoggedIn()) graphIdToTagMap[R.id.login] else graphIdToTagMap[item.itemId] - - fragmentManager.popBackStack( - firstFragmentTag, - FragmentManager.POP_BACK_STACK_INCLUSIVE - ) - val selectedFragment = - fragmentManager.findFragmentByTag(newlySelectedItemTag) as NavHostFragment - - // Exclude the first fragment tag because it's always in the back stack. - if (firstFragmentTag != newlySelectedItemTag) { - // Commit a transaction that cleans the back stack and adds the first fragment - // to it, creating the fixed started destination. - fragmentManager.beginTransaction() - .setCustomAnimations( - R.anim.nav_default_enter_anim, - R.anim.nav_default_exit_anim, - R.anim.nav_default_pop_enter_anim, - R.anim.nav_default_pop_exit_anim - ) - .attach(selectedFragment) - .setPrimaryNavigationFragment(selectedFragment) - .apply { - // Detach all other Fragments - graphIdToTagMap.forEach { _, fragmentTagIter -> - if (fragmentTagIter != newlySelectedItemTag) { - detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!) - } - } - } - .addToBackStack(firstFragmentTag) - .setReorderingAllowed(true) - .commit() - } - selectedItemTag = newlySelectedItemTag - isOnFirstFragment = selectedItemTag == firstFragmentTag - selectedNavController.value = selectedFragment.navController - true - } else { - false - } - } - } - - setOnItemReselectedListener { item -> - val newlySelectedItemTag = graphIdToTagMap[item.itemId] - val selectedFragment = - fragmentManager.findFragmentByTag(newlySelectedItemTag) as NavHostFragment - val navController = selectedFragment.navController - // Pop the back stack to the start destination of the current navController graph - if (selectedItemTag != graphIdToTagMap[item.itemId]) { - fragmentManager.beginTransaction() - .setCustomAnimations( - R.anim.nav_default_enter_anim, - R.anim.nav_default_exit_anim, - R.anim.nav_default_pop_enter_anim, - R.anim.nav_default_pop_exit_anim - ) - .attach(selectedFragment) - .setPrimaryNavigationFragment(selectedFragment) - .apply { - // Detach all other Fragments - graphIdToTagMap.forEach { _, fragmentTagIter -> - if (fragmentTagIter != newlySelectedItemTag) { - detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!) - } - } - } - .addToBackStack(firstFragmentTag) - .setReorderingAllowed(true) - .commit() - selectedItemTag = newlySelectedItemTag - isOnFirstFragment = selectedItemTag == firstFragmentTag - selectedNavController.value = selectedFragment.navController - } else navController.popBackStack(navController.graph.startDestination, false) - } - // Optional: on item reselected, pop back stack to the destination of the graph - setupDeepLinks(navGraphIds, fragmentManager, containerId, intent) - - // Finally, ensure that we update our BottomNavigationView when the back stack changes - fragmentManager.addOnBackStackChangedListener { - if (!isOnFirstFragment && !fragmentManager.isOnBackStack(firstFragmentTag)) { - this.selectedItemId = firstFragmentGraphId - } - - // Reset the graph if the currentDestination is not valid (happens when the back - // stack is popped after using the back button). - selectedNavController.value?.let { controller -> - if (controller.currentDestination == null) { - controller.navigate(controller.graph.id) - } - } - } - return selectedNavController - } - - private fun BottomNavigationView.setupDeepLinks( - navGraphIds: List, - fragmentManager: FragmentManager, - containerId: Int, - intent: Intent - ) { - navGraphIds.forEachIndexed { index, navGraphId -> - val fragmentTag = getFragmentTag(index) - - // Find or create the Navigation host fragment - val navHostFragment = obtainNavHostFragment( - fragmentManager, - fragmentTag, - navGraphId, - containerId - ) - // Handle Intent - if (navHostFragment.navController.handleDeepLink(intent) && - selectedItemId != navHostFragment.navController.graph.id - ) { - this.selectedItemId = navHostFragment.navController.graph.id - } - } - } - - private fun detachNavHostFragment( - fragmentManager: FragmentManager, - navHostFragment: NavHostFragment - ) { - fragmentManager.beginTransaction() - .detach(navHostFragment) - .commitNow() - } - - private fun attachNavHostFragment( - fragmentManager: FragmentManager, - navHostFragment: NavHostFragment, - isPrimaryNavFragment: Boolean - ) { - fragmentManager.beginTransaction() - .attach(navHostFragment) - .apply { - if (isPrimaryNavFragment) { - setPrimaryNavigationFragment(navHostFragment) - } - } - .commitNow() - } - - private fun obtainNavHostFragment( - fragmentManager: FragmentManager, - fragmentTag: String, - navGraphId: Int, - containerId: Int, - ): NavHostFragment { - // If the Nav Host fragment exists, return it - val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment? - existingFragment?.let { return it } - - // Otherwise, create it and return it. - val navHostFragment = NavHostFragment.create(navGraphId) - fragmentManager.beginTransaction() - .add(containerId, navHostFragment, fragmentTag) - .commitNow() - return navHostFragment - } - - private fun FragmentManager.isOnBackStack(backStackName: String): Boolean { - val backStackCount = backStackEntryCount - for (index in 0 until backStackCount) { - if (getBackStackEntryAt(index).name == backStackName) { - return true - } - } - return false - } - - val FragmentManager.visibleFragments - get(): List { - val visibleFragments = arrayListOf() - fragments.forEach { if (it.isVisible) visibleFragments.add(it) } - return visibleFragments - } - - private fun getFragmentTag(index: Int) = "bottomNavigation#$index" -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/extensions/TextViewExtensions.kt b/app/src/main/kotlin/com/meloda/fast/extensions/TextViewExtensions.kt deleted file mode 100644 index b4a7ce18..00000000 --- a/app/src/main/kotlin/com/meloda/fast/extensions/TextViewExtensions.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.extensions - -import android.widget.TextView - -object TextViewExtensions { - - fun TextView.clear() { - text = null - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/model/ListModels.kt b/app/src/main/kotlin/com/meloda/fast/model/ListModels.kt new file mode 100644 index 00000000..f381fe69 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/model/ListModels.kt @@ -0,0 +1,13 @@ +package com.meloda.fast.model + +sealed class DataItem { + abstract val dataItemId: IdType + + object Header : DataItem() { + override val dataItemId = Int.MIN_VALUE + } + + object Footer : DataItem() { + override val dataItemId = Int.MIN_VALUE + 1 + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/model/SelectableItem.kt b/app/src/main/kotlin/com/meloda/fast/model/SelectableItem.kt new file mode 100644 index 00000000..e17b0aa4 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/model/SelectableItem.kt @@ -0,0 +1,22 @@ +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 constructor( + @Ignore + val selectableItemId: Int = 0 +) : DataItem(), Parcelable { + + @Ignore + @IgnoredOnParcel + var isSelected: Boolean = false + + @Ignore + @IgnoredOnParcel + override val dataItemId = selectableItemId + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsAdapter.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsAdapter.kt index 9329923a..24803dac 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsAdapter.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsAdapter.kt @@ -8,10 +8,10 @@ import android.text.TextUtils import android.text.style.ForegroundColorSpan import android.view.ViewGroup import androidx.core.content.ContextCompat +import androidx.core.util.ObjectsCompat import androidx.core.view.isVisible import androidx.core.view.setPadding import androidx.recyclerview.widget.DiffUtil -import coil.load import com.meloda.fast.R import com.meloda.fast.api.UserConfig import com.meloda.fast.api.VKConstants @@ -22,26 +22,35 @@ import com.meloda.fast.api.model.VkUser import com.meloda.fast.base.adapter.BaseAdapter import com.meloda.fast.base.adapter.BindingHolder import com.meloda.fast.databinding.ItemConversationBinding +import com.meloda.fast.extensions.ImageLoader +import com.meloda.fast.extensions.ImageLoader.clear +import com.meloda.fast.extensions.ImageLoader.loadWithGlide +import com.meloda.fast.extensions.gone +import com.meloda.fast.extensions.toggleVisibility +import com.meloda.fast.extensions.visible import com.meloda.fast.util.TimeUtils class ConversationsAdapter constructor( context: Context, - values: MutableList, + private val resourceManager: ConversationsResourceManager, + var isMultilineEnabled: Boolean = true, val profiles: HashMap = hashMapOf(), val groups: HashMap = hashMapOf(), - var isMultilineEnabled: Boolean = true -) : BaseAdapter( - context, values, COMPARATOR -) { +) : BaseAdapter(context, Comparator) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - ItemHolder(ItemConversationBinding.inflate(inflater, parent, false)) + var pinnedCount = 0 - inner class ItemHolder(binding: ItemConversationBinding) : - BindingHolder(binding) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder { + return ItemHolder( + ItemConversationBinding.inflate(inflater, parent, false), + resourceManager + ) + } - private val dateColor = ContextCompat.getColor(context, R.color.n2_500) - private val youPrefix = context.getString(R.string.you_message_prefix) + inner class ItemHolder( + binding: ItemConversationBinding, + private val resourceManager: ConversationsResourceManager + ) : BindingHolder(binding) { init { binding.title.ellipsize = TextUtils.TruncateAt.END @@ -69,7 +78,7 @@ class ConversationsAdapter constructor( ) val span = SpannableString(text) - span.setSpan(ForegroundColorSpan(dateColor), 0, text.length, 0) + span.setSpan(ForegroundColorSpan(resourceManager.colorOutline), 0, text.length, 0) binding.message.text = span return @@ -87,51 +96,46 @@ class ConversationsAdapter constructor( conversationGroup = conversationGroup ) - binding.avatar.isVisible = avatar != null + binding.avatar.toggleVisibility(avatar != null) if (avatar == null) { - binding.avatarPlaceholder.isVisible = true + binding.avatarPlaceholder.visible() if (conversation.ownerId == VKConstants.FAST_GROUP_ID) { - binding.placeholderBack.setImageDrawable( - ColorDrawable( - ContextCompat.getColor(context, R.color.a1_400) - ) + binding.placeholderBack.loadWithGlide( + drawable = ColorDrawable(resourceManager.icLauncherColor), + transformations = ImageLoader.userAvatarTransformations ) binding.placeholder.imageTintList = - ColorStateList.valueOf(ContextCompat.getColor(context, R.color.a1_0)) + ColorStateList.valueOf(resourceManager.colorOnPrimary) binding.placeholder.setImageResource(R.drawable.ic_fast_logo) binding.placeholder.setPadding(18) } else { - binding.placeholderBack.setImageDrawable( - ColorDrawable( - ContextCompat.getColor(context, R.color.n1_50) - ) + binding.placeholderBack.loadWithGlide( + drawable = ColorDrawable(resourceManager.colorOnUserAvatarAction), + transformations = ImageLoader.userAvatarTransformations ) binding.placeholder.imageTintList = - ColorStateList.valueOf(ContextCompat.getColor(context, R.color.n2_500)) + ColorStateList.valueOf(resourceManager.colorUserAvatarAction) binding.placeholder.setImageResource(R.drawable.ic_account_circle_cut) binding.placeholder.setPadding(0) - binding.avatar.setImageDrawable(null) + binding.avatar.clear() } } else { - binding.avatar.load(avatar) { - crossfade(200) - target { - binding.avatarPlaceholder.isVisible = false - binding.avatar.setImageDrawable(it) - } - } + binding.avatar.loadWithGlide( + url = avatar, + crossFade = true, + onLoadedAction = { binding.avatarPlaceholder.gone() } + ) } - binding.online.isVisible = conversationUser?.online == true - - binding.pin.isVisible = conversation.isPinned + binding.online.toggleVisibility(conversationUser?.online == true) + binding.pin.toggleVisibility(conversation.isPinned) val actionMessage = VkUtils.getActionConversationText( context = context, message = message, - youPrefix = youPrefix, + youPrefix = resourceManager.youPrefix, profiles = profiles, groups = groups, messageUser = messageUser, @@ -150,7 +154,7 @@ class ConversationsAdapter constructor( message = message ) - binding.textAttachment.isVisible = attachmentIcon != null + binding.textAttachment.toggleVisibility(attachmentIcon != null) binding.textAttachment.setImageDrawable(attachmentIcon) val attachmentText = if (attachmentIcon == null) VkUtils.getAttachmentText( @@ -174,7 +178,7 @@ class ConversationsAdapter constructor( var prefix = when { actionMessage != null -> "" - message.isOut -> "$youPrefix: " + message.isOut -> "${resourceManager.youPrefix}: " else -> { if (message.isUser() && messageUser != null && messageUser.firstName.isNotBlank()) "${messageUser.firstName}: " else if (message.isGroup() && messageGroup != null && messageGroup.name.isNotBlank()) "${messageGroup.name}: " @@ -190,7 +194,7 @@ class ConversationsAdapter constructor( val spanMessage = SpannableString(spanText) spanMessage.setSpan( - ForegroundColorSpan(dateColor), 0, + ForegroundColorSpan(resourceManager.colorOutline), 0, prefix.length + coloredMessage.length, 0 ) @@ -208,6 +212,15 @@ class ConversationsAdapter constructor( R.drawable.ic_message_unread ) else null + binding.onlineBorder.setImageDrawable( + ColorDrawable( + ContextCompat.getColor( + context, + if (conversation.isUnread()) R.color.colorBackgroundVariant + else R.color.colorBackground + ) + ) + ) binding.counter.isVisible = conversation.isInUnread() if (conversation.isInUnread()) { @@ -222,10 +235,10 @@ class ConversationsAdapter constructor( } fun removeConversation(conversationId: Int): Int? { - for (i in values.indices) { - val conversation = values[i] + for (i in indices) { + val conversation = getItem(i) if (conversation.id == conversationId) { - values.removeAt(i) + removeAt(i) return i } } @@ -233,17 +246,29 @@ class ConversationsAdapter constructor( return null } + fun searchConversationIndex(conversationId: Int): Int? { + for (i in indices) { + val conversation = getItem(i) + + if (conversation.id == conversationId) return i + } + + return null + } + companion object { - private val COMPARATOR = object : DiffUtil.ItemCallback() { + private val Comparator = object : DiffUtil.ItemCallback() { override fun areItemsTheSame( oldItem: VkConversation, newItem: VkConversation - ) = false + ): Boolean { + return oldItem.id == newItem.id + } override fun areContentsTheSame( oldItem: VkConversation, newItem: VkConversation - ) = false + ) = ObjectsCompat.equals(oldItem, newItem) } } 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 index e63621a3..42a9cbea 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt @@ -1,23 +1,16 @@ package com.meloda.fast.screens.conversations -import android.content.Intent import android.os.Bundle import android.view.Gravity import android.view.View import android.viewbinding.library.fragment.viewBinding import androidx.appcompat.widget.PopupMenu import androidx.core.os.bundleOf -import androidx.core.view.isVisible -import androidx.core.view.updatePadding import androidx.datastore.preferences.core.edit import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController -import coil.load -import com.google.android.material.appbar.AppBarLayout import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.meloda.fast.R -import com.meloda.fast.activity.MainActivity import com.meloda.fast.api.UserConfig import com.meloda.fast.api.model.VkConversation import com.meloda.fast.base.BaseViewModelFragment @@ -28,14 +21,16 @@ import com.meloda.fast.common.AppGlobal import com.meloda.fast.common.AppSettings import com.meloda.fast.common.dataStore import com.meloda.fast.databinding.FragmentConversationsBinding +import com.meloda.fast.extensions.ImageLoader.loadWithGlide +import com.meloda.fast.extensions.gone +import com.meloda.fast.extensions.toggleVisibility +import com.meloda.fast.screens.messages.MessagesHistoryFragment import com.meloda.fast.util.AndroidUtils import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import kotlin.math.abs -import kotlin.math.roundToInt @AndroidEntryPoint class ConversationsFragment : @@ -47,9 +42,7 @@ class ConversationsFragment : private val adapter: ConversationsAdapter by lazy { ConversationsAdapter( requireContext(), - mutableListOf(), - hashMapOf(), - hashMapOf() + ConversationsResourceManager(requireContext()) ).also { it.itemClickListener = this::onItemClick it.itemLongClickListener = this::onItemLongClick @@ -74,13 +67,6 @@ class ConversationsFragment : } } - private var isPaused = false - - override fun onPause() { - super.onPause() - isPaused = true - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) prepareViews() @@ -90,41 +76,16 @@ class ConversationsFragment : lifecycleScope.launch { requireContext().dataStore.data.map { adapter.isMultilineEnabled = it[AppSettings.keyIsMultilineEnabled] ?: true - adapter.notifyItemRangeChanged(0, adapter.itemCount) + adapter.refreshList() }.collect() } binding.createChat.setOnClickListener {} - UserConfig.vkUser.observe(viewLifecycleOwner) { - it?.let { user -> binding.avatar.load(user.photo200) { crossfade(100) } } + UserConfig.vkUser.observe(viewLifecycleOwner) { user -> + user?.run { binding.avatar.loadWithGlide(url = this.photo200, crossFade = true) } } - binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset -> - if (isPaused) return@OnOffsetChangedListener - - binding.appBar.animate().translationZ( - if (verticalOffset < 0) AndroidUtils.px(3).roundToInt().toFloat() - else 0f - ).setDuration(50).start() - - val padding = AndroidUtils.px(if (verticalOffset <= -100) 10 else 30).roundToInt() - - binding.avatarContainer.updatePadding( - bottom = padding, - right = padding - ) - - val minusAlpha = (1 - (abs(verticalOffset) * 0.01)).toFloat() - val plusAlpha = (abs(1 + verticalOffset * 0.01) * 1.01).toFloat() - - println("Fast::ConversationsFragment::onOffset offset: $verticalOffset; minusAlpha: $minusAlpha; plusAlpha: $plusAlpha") - - val alpha: Float = if (verticalOffset <= -100) plusAlpha else minusAlpha - - binding.avatarContainer.alpha = alpha - }) - binding.avatar.setOnClickListener { avatarPopupMenu.show() } binding.avatar.setOnLongClickListener { @@ -134,23 +95,18 @@ class ConversationsFragment : settings[AppSettings.keyIsMultilineEnabled] = !isMultilineEnabled adapter.isMultilineEnabled = !isMultilineEnabled - adapter.notifyItemRangeChanged(0, adapter.itemCount) + adapter.refreshList() } } true } - if (isPaused) { - isPaused = false - return - } - viewModel.loadProfileUser() viewModel.loadConversations() } private fun showLogOutDialog() { - val isEasterEgg = UserConfig.userId == UserConfig.userId + val isEasterEgg = UserConfig.userId == 37610580 MaterialAlertDialogBuilder(requireContext()) .setTitle( @@ -166,13 +122,7 @@ class ConversationsFragment : UserConfig.clear() AppGlobal.appDatabase.clearAllTables() - requireActivity().finishAffinity() - requireActivity().startActivity( - Intent( - requireContext(), - MainActivity::class.java - ) - ) + viewModel.openRootScreen() } } .setNegativeButton(R.string.no, null) @@ -185,21 +135,31 @@ class ConversationsFragment : is StartProgressEvent -> onProgressStarted() is StopProgressEvent -> onProgressStopped() - is ConversationsLoaded -> refreshConversations(event) - is ConversationsDelete -> deleteConversation(event.peerId) + is ConversationsLoadedEvent -> refreshConversations(event) + is ConversationsDeleteEvent -> deleteConversation(event.peerId) // TODO: 10-Oct-21 remove this and sort conversations list - is ConversationsPin, is ConversationsUnpin -> viewModel.loadConversations() + is ConversationsPinEvent -> { + adapter.pinnedCount++ + viewModel.loadConversations() + } + is ConversationsUnpinEvent -> { + adapter.pinnedCount-- + viewModel.loadConversations() + } + + is MessagesNewEvent -> onMessageNew(event) + is MessagesEditEvent -> onMessageEdit(event) } } private fun onProgressStarted() { - binding.progressBar.isVisible = adapter.isEmpty() + binding.progressBar.toggleVisibility(adapter.isEmpty()) binding.refreshLayout.isRefreshing = adapter.isNotEmpty() } private fun onProgressStopped() { - binding.progressBar.isVisible = false + binding.progressBar.gone() binding.refreshLayout.isRefreshing = false } @@ -233,16 +193,17 @@ class ConversationsFragment : } } - private fun refreshConversations(event: ConversationsLoaded) { + private fun refreshConversations(event: ConversationsLoadedEvent) { adapter.profiles += event.profiles adapter.groups += event.groups + val pinnedConversations = event.conversations.filter { it.isPinned } + adapter.pinnedCount = pinnedConversations.count() + fillRecyclerView(event.conversations) } private fun fillRecyclerView(values: List) { - adapter.values.clear() - adapter.values += values adapter.submitList(values) } @@ -257,12 +218,11 @@ class ConversationsFragment : if (conversation.isGroup()) adapter.groups[conversation.id] else null - findNavController().navigate( - R.id.toMessagesHistory, + viewModel.openMessagesHistoryScreen( bundleOf( - "conversation" to adapter[position], - "user" to user, - "group" to group + MessagesHistoryFragment.ARG_USER to user, + MessagesHistoryFragment.ARG_GROUP to group, + MessagesHistoryFragment.ARG_CONVERSATION to conversation ) ) } @@ -277,7 +237,7 @@ class ConversationsFragment : var canPinOneMoreDialog = true if (adapter.itemCount > 4) { - val firstFiveDialogs = adapter.values.subList(0, 5) + val firstFiveDialogs = adapter.currentList.subList(0, 5) var pinnedCount = 0 firstFiveDialogs.forEach { if (it.isPinned) pinnedCount++ } @@ -321,8 +281,7 @@ class ConversationsFragment : } private fun deleteConversation(conversationId: Int) { - val index = adapter.removeConversation(conversationId) ?: return - adapter.notifyItemRemoved(index) + adapter.removeConversation(conversationId) } private fun showPinConversationDialog(conversation: VkConversation) { @@ -345,4 +304,45 @@ class ConversationsFragment : .show() } + private fun onMessageNew(event: MessagesNewEvent) { + adapter.profiles += event.profiles + adapter.groups += event.groups + + val message = event.message + + val conversationIndex = adapter.searchConversationIndex(message.peerId) + if (conversationIndex == null) { // диалога нет в списке + + } else { + val conversation = adapter[conversationIndex] + conversation.run { + lastMessage = message + lastMessageId = message.id + lastConversationMessageId = -1 + } + + if (conversation.isPinned) { + adapter[conversationIndex] = conversation + return + } + + adapter.removeConversation(message.peerId) ?: return + val toPosition = adapter.pinnedCount + + adapter.add(conversation, toPosition) + } + } + + private fun onMessageEdit(event: MessagesEditEvent) { + val message = event.message + + val conversationIndex = adapter.searchConversationIndex(message.peerId) + if (conversationIndex == null) { // диалога нет в списке + + } else { + val conversation = adapter[conversationIndex] + conversation.lastMessage = message + adapter[conversationIndex] = conversation + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsResourceManager.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsResourceManager.kt new file mode 100644 index 00000000..c561a4c4 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsResourceManager.kt @@ -0,0 +1,19 @@ +package com.meloda.fast.screens.conversations + +import android.content.Context +import com.meloda.fast.R +import com.meloda.fast.base.ResourceManager +import com.meloda.fast.extensions.TypeTransformations + +class ConversationsResourceManager(context: Context) : ResourceManager(context) { + + 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 icLauncherColor = getColor(R.color.a1_500) + + val youPrefix = getString(R.string.you_message_prefix) + +} \ No newline at end of file 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 index c750edd0..75dc08d9 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt @@ -1,28 +1,49 @@ package com.meloda.fast.screens.conversations +import android.os.Bundle import androidx.lifecycle.viewModelScope +import com.github.terrakok.cicerone.Router +import com.meloda.fast.api.LongPollEvent +import com.meloda.fast.api.LongPollUpdatesParser import com.meloda.fast.api.UserConfig import com.meloda.fast.api.VKConstants import com.meloda.fast.api.model.VkConversation 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.network.conversations.* import com.meloda.fast.api.network.users.UsersDataSource import com.meloda.fast.api.network.users.UsersGetRequest import com.meloda.fast.base.viewmodel.BaseViewModel import com.meloda.fast.base.viewmodel.VkEvent +import com.meloda.fast.common.Screens import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import java.util.* import javax.inject.Inject @HiltViewModel class ConversationsViewModel @Inject constructor( private val conversations: ConversationsDataSource, - private val users: UsersDataSource + private val users: UsersDataSource, + updatesParser: LongPollUpdatesParser, + private val router: Router ) : BaseViewModel() { + companion object { + private const val TAG = "ConversationsViewModel" + } + + init { + updatesParser.onNewMessage { + viewModelScope.launch { handleNewMessage(it) } + } + + updatesParser.onMessageEdited { + viewModelScope.launch { handleEditedMessage(it) } + } + } + fun loadConversations( offset: Int? = null ) = viewModelScope.launch(Dispatchers.Default) { @@ -49,7 +70,7 @@ class ConversationsViewModel @Inject constructor( } sendEvent( - ConversationsLoaded( + ConversationsLoadedEvent( count = response.count, offset = offset, unreadCount = response.unreadCount ?: 0, @@ -84,7 +105,7 @@ class ConversationsViewModel @Inject constructor( conversations.delete( ConversationsDeleteRequest(peerId) ) - }, onAnswer = { sendEvent(ConversationsDelete(peerId)) }) + }, onAnswer = { sendEvent(ConversationsDeleteEvent(peerId)) }) } fun pinConversation( @@ -94,18 +115,41 @@ class ConversationsViewModel @Inject constructor( if (pin) { makeJob( { conversations.pin(ConversationsPinRequest(peerId)) }, - onAnswer = { sendEvent(ConversationsPin(peerId)) } + onAnswer = { sendEvent(ConversationsPinEvent(peerId)) } ) } else { makeJob( { conversations.unpin(ConversationsUnpinRequest(peerId)) }, - onAnswer = { sendEvent(ConversationsUnpin(peerId)) } + onAnswer = { sendEvent(ConversationsUnpinEvent(peerId)) } ) } } + + private suspend fun handleNewMessage(event: LongPollEvent.VkMessageNewEvent) { + sendEvent( + MessagesNewEvent( + message = event.message, + profiles = event.profiles, + groups = event.groups + ) + ) + } + + private suspend fun handleEditedMessage(event: LongPollEvent.VkMessageEditEvent) { + sendEvent(MessagesEditEvent(event.message)) + } + + fun openRootScreen() { + router.exit() + router.newRootScreen(Screens.Main()) + } + + fun openMessagesHistoryScreen(bundle: Bundle) { + router.navigateTo(Screens.MessagesHistory(bundle)) + } } -data class ConversationsLoaded( +data class ConversationsLoadedEvent( val count: Int, val offset: Int?, val unreadCount: Int?, @@ -114,8 +158,16 @@ data class ConversationsLoaded( val groups: HashMap ) : VkEvent() -data class ConversationsDelete(val peerId: Int) : VkEvent() +data class ConversationsDeleteEvent(val peerId: Int) : VkEvent() -data class ConversationsPin(val peerId: Int) : VkEvent() +data class ConversationsPinEvent(val peerId: Int) : VkEvent() -data class ConversationsUnpin(val peerId: Int) : VkEvent() \ No newline at end of file +data class ConversationsUnpinEvent(val peerId: Int) : VkEvent() + +data class MessagesNewEvent( + val message: VkMessage, + val profiles: HashMap, + val groups: HashMap +) : VkEvent() + +data class MessagesEditEvent(val message: VkMessage) : VkEvent() \ No newline at end of file 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 index d808d711..14169b8e 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt @@ -18,7 +18,6 @@ import androidx.core.view.isVisible import androidx.core.widget.addTextChangedListener import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController import coil.load import coil.transform.RoundedCornersTransformation import com.google.android.material.snackbar.Snackbar @@ -35,9 +34,7 @@ import com.meloda.fast.databinding.FragmentLoginBinding import com.meloda.fast.util.KeyboardUtils import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.jsoup.Jsoup import java.net.URLEncoder import java.util.* import java.util.regex.Pattern @@ -77,7 +74,7 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo is ErrorEvent -> showErrorSnackbar(event.errorText) is CaptchaEvent -> showCaptchaDialog(event.sid, event.image) is ValidationEvent -> showValidationRequired(event.sid) - is SuccessAuth -> goToMain(event) + is SuccessAuth -> launchWebView() is CodeSent -> showValidationDialog() is StartProgressEvent -> onProgressStarted() @@ -119,12 +116,8 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo parseAuthUrl(url) } - override fun onPageFinished(view: WebView, url: String) { + override fun onPageFinished(view: WebView, url: String?) { super.onPageFinished(view, url) - - val a = Jsoup.parse(url) - - val b = 0 } } } @@ -137,15 +130,23 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo } private fun launchWebView() { + binding.webView.isVisible = true binding.webView.loadUrl( "https://oauth.vk.com/authorize?client_id=${UserConfig.FAST_APP_ID}&" + - "display=mobile&scope=136297695&" + + "access_token=${UserConfig.accessToken}&" + + "sdk_package=com.meloda.fast.activity&" + + "sdk_fingerprint=AA88DSADAS8DG8FSA8&" + + "display=page&" + + "revoke=1&" + + "scope=136297695&" + "redirect_uri=${ URLEncoder.encode( "https://oauth.vk.com/blank.html", Charsets.UTF_8.toString() ) - }&response_type=token&v=${VKConstants.API_VERSION}" + }&" + + "response_type=token&" + + "v=${VKConstants.API_VERSION}" ) } @@ -167,6 +168,8 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo val token = authData.first UserConfig.fastToken = token + + viewModel.openPrimaryScreen() } } @@ -205,9 +208,9 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo else TextInputLayout.END_ICON_NONE } - binding.passwordInput.setOnEditorActionListener { _, _, event -> - if (event == null) return@setOnEditorActionListener false - return@setOnEditorActionListener if (event.action == EditorInfo.IME_ACTION_GO || + binding.passwordInput.setOnEditorActionListener edit@{ _, _, event -> + if (event == null) return@edit false + return@edit if (event.action == EditorInfo.IME_ACTION_GO || (event.action == KeyEvent.ACTION_DOWN && (event.keyCode == KeyEvent.KEYCODE_ENTER || event.keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER)) ) { KeyboardUtils.hideKeyboardFrom(binding.passwordInput) @@ -237,7 +240,6 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo KeyboardUtils.hideKeyboardFrom(requireView().findFocus()) - viewModel.login( login = loginString, password = passwordString @@ -383,16 +385,4 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo snackbar.animationMode = Snackbar.ANIMATION_MODE_FADE snackbar.show() } - - private fun goToMain(event: SuccessAuth) = lifecycleScope.launch { - UserConfig.userId = event.userId - UserConfig.accessToken = event.vkToken - - if (event.haveAuthorized) delay(500) - - launchWebView() - - findNavController().navigate(R.id.toMain) - } - } \ No newline at end of file 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 index c4666d32..53a55fbb 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt @@ -1,20 +1,30 @@ 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.VKException import com.meloda.fast.api.network.auth.AuthDataSource -import com.meloda.fast.api.network.auth.RequestAuthDirect -import com.meloda.fast.base.viewmodel.* +import com.meloda.fast.api.network.auth.AuthDirectRequest +import com.meloda.fast.base.viewmodel.BaseViewModel +import com.meloda.fast.base.viewmodel.ErrorEvent +import com.meloda.fast.base.viewmodel.VkEvent +import com.meloda.fast.common.Screens import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor( - private val dataSource: AuthDataSource + private val dataSource: AuthDataSource, + private val router: Router ) : BaseViewModel() { + companion object { + private const val TAG = "LoginViewModel" + } + fun login( login: String, password: String, @@ -24,7 +34,7 @@ class LoginViewModel @Inject constructor( makeJob( { dataSource.auth( - RequestAuthDirect( + AuthDirectRequest( grantType = VKConstants.Auth.GrantType.PASSWORD, clientId = VKConstants.VK_APP_ID, clientSecret = VKConstants.VK_SECRET, @@ -44,12 +54,24 @@ class LoginViewModel @Inject constructor( return@makeJob } - sendEvent( - SuccessAuth( - userId = it.userId, - vkToken = it.accessToken - ) - ) + UserConfig.userId = it.userId + UserConfig.accessToken = it.accessToken + + sendEvent(SuccessAuth()) + + // TODO: 19-Oct-21 do somewhen +// makeJob({ +// dataSource.authWithApp( +// AuthWithAppRequest( +// accessToken = it.accessToken +// ) +// ) +// }, onAnswer = { kindaAnswer -> +// println("$TAG: AppAuthResponse: $kindaAnswer") +// } +// ) + + }, onError = { if (it !is VKException) { @@ -69,12 +91,14 @@ class LoginViewModel @Inject constructor( ) } + fun openPrimaryScreen() { + router.navigateTo(Screens.Conversations()) + } + } object CodeSent : VkEvent() data class SuccessAuth( - val haveAuthorized: Boolean = true, - val userId: Int, - val vkToken: String + val haveAuthorized: Boolean = true ) : VkEvent() \ No newline at end of file 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 index 980c9f8f..727f80d5 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt @@ -1,45 +1,29 @@ package com.meloda.fast.screens.main import android.os.Bundle +import android.view.LayoutInflater import android.view.View -import android.viewbinding.library.fragment.viewBinding +import android.view.ViewGroup import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import com.meloda.fast.R -import com.meloda.fast.api.UserConfig import com.meloda.fast.base.BaseViewModelFragment -import com.meloda.fast.databinding.FragmentMainBinding -import com.meloda.fast.extensions.NavigationExtensions.setupWithNavController import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class MainFragment : BaseViewModelFragment(R.layout.fragment_main) { +class MainFragment : BaseViewModelFragment() { override val viewModel: MainViewModel by viewModels() - private val binding: FragmentMainBinding by viewBinding() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return View(context) + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - if (!UserConfig.isLoggedIn()) findNavController().navigate(R.id.toLogin) - else if (savedInstanceState == null) setupBottomBar() + viewModel.checkSession(requireContext()) } - - private fun setupBottomBar() { - val navGraphIds = listOf( - R.navigation.messages, - R.navigation.login - ) - - with(binding.bottomBar) { - selectedItemId = R.id.messages - setupWithNavController( - navGraphIds = navGraphIds, - fragmentManager = childFragmentManager, - containerId = R.id.fragmentContainer, - intent = requireActivity().intent - ) - } - } - } \ No newline at end of file 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 index af5c0d73..96fd48ac 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/MainViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/main/MainViewModel.kt @@ -1,5 +1,30 @@ package com.meloda.fast.screens.main +import android.content.Context +import android.content.Intent +import com.github.terrakok.cicerone.Router +import com.meloda.fast.api.UserConfig import com.meloda.fast.base.viewmodel.BaseViewModel +import com.meloda.fast.common.Screens +import com.meloda.fast.service.MessagesUpdateService +import com.meloda.fast.service.OnlineService +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject -class MainViewModel : BaseViewModel() \ No newline at end of file +@HiltViewModel +class MainViewModel @Inject constructor(private val router: Router) : BaseViewModel() { + + fun checkSession(context: Context) { + if (UserConfig.isLoggedIn()) { + router.navigateTo(Screens.Conversations()) + + context.run { + startService(Intent(this, MessagesUpdateService::class.java)) + startService(Intent(this, OnlineService::class.java)) + } + } else { + router.navigateTo(Screens.Login()) + } + } + +} \ No newline at end of file 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 index ca561535..a12ece1c 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt @@ -1,24 +1,19 @@ package com.meloda.fast.screens.messages import android.content.Context -import android.content.res.ColorStateList +import android.content.res.Resources +import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.util.Log -import android.view.Gravity import android.view.LayoutInflater import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.ImageView import android.widget.Space -import androidx.appcompat.widget.AppCompatImageView 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.setPadding -import androidx.core.view.updatePadding -import coil.load -import com.google.android.material.imageview.ShapeableImageView +import androidx.core.view.* +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 @@ -27,8 +22,10 @@ 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.databinding.* +import com.meloda.fast.extensions.* +import com.meloda.fast.extensions.ImageLoader.clear +import com.meloda.fast.extensions.ImageLoader.loadWithGlide import com.meloda.fast.util.AndroidUtils -import com.meloda.fast.widget.RoundedFrameLayout import java.text.SimpleDateFormat import java.util.* import kotlin.math.roundToInt @@ -46,10 +43,18 @@ class AttachmentInflater constructor( private val inflater = LayoutInflater.from(context) - private val playColor = ContextCompat.getColor(context, R.color.a3_700) - private val playBackgroundColor = ContextCompat.getColor(context, R.color.a3_200) + private val colorBackground = ContextCompat.getColor( + context, + R.color.colorBackground + ) + private val colorSecondary = ContextCompat.getColor( + context, + R.color.colorSecondary + ) - var photoClickListener: ((url: String) -> Unit)? = null + private var photoClickListener: ((url: String) -> Unit)? = null + + private val displayMetrics get() = Resources.getSystem().displayMetrics fun setPhotoClickListener(unit: ((url: String) -> Unit)?): AttachmentInflater { this.photoClickListener = unit @@ -57,15 +62,15 @@ class AttachmentInflater constructor( } fun inflate() { - if (message.attachments.isNullOrEmpty()) return - attachments = message.attachments!! - container.removeAllViews() if (textContainer.childCount > 1) { textContainer.removeViews(1, textContainer.childCount - 1) } + if (message.attachments.isNullOrEmpty()) return + attachments = message.attachments!! + if (attachments.size == 1) { when (val attachment = attachments[0]) { is VkSticker -> return sticker(attachment) @@ -74,6 +79,7 @@ class AttachmentInflater constructor( is VkCall -> return call(attachment) is VkGraffiti -> return graffiti(attachment) is VkGift -> return gift(attachment) + is VkStory -> return story(attachment) } } @@ -113,112 +119,107 @@ class AttachmentInflater constructor( } private fun photo(photo: VkPhoto) { - val size = photo.getSizeOrSmaller('y') ?: return + val size = photo.getSizeOrSmaller(VkPhoto.SIZE_TYPE_807) ?: return - val newPhoto = ShapeableImageView(context).apply { - layoutParams = LinearLayoutCompat.LayoutParams( -// ViewGroup.LayoutParams.MATCH_PARENT, - size.width, - size.height -// AndroidUtils.px(size.width).roundToInt(), -// AndroidUtils.px(size.height).roundToInt() - ) + 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}" - shapeAppearanceModel = - shapeAppearanceModel.withCornerSize { - AndroidUtils.px(5) - } - - scaleType = ImageView.ScaleType.CENTER_CROP + val spacer = Space(context).apply { + layoutParams = + LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 5.dpToPx()) } - if (photoClickListener != null) { - newPhoto.setOnClickListener { photoClickListener?.invoke(size.url) } - } else { - newPhoto.setOnClickListener(null) - } - - val spacer = Space(context).also { - it.layoutParams = LinearLayoutCompat.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - AndroidUtils.px(5).roundToInt() - ) - } - - if (container.isNotEmpty()) + if (container.isNotEmpty()) { container.addView(spacer) + } - if (attachments.size == 1) { - val roundedLayout = RoundedFrameLayout(context).apply { - setTopRightCornerRadius((if (message.isOut) 30 else 40).toFloat()) - setTopLeftCornerRadius((if (message.isOut) 40 else 30).toFloat()) - setBottomRightCornerRadius((if (message.isOut) 5 else 40).toFloat()) - setBottomLeftCornerRadius((if (message.isOut) 40 else 5).toFloat()) + val binding = ItemMessageAttachmentPhotoBinding.inflate(inflater, container, true) + + val cornersRadius = 8.dpToPx().toFloat() + + binding.border.run { + shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius) + + updateLayoutParams { + width = (displayMetrics.widthPixels * widthMultiplier).roundToInt() + dimensionRatio = ratio + } + loadWithGlide( + drawable = ColorDrawable(colorSecondary), + priority = Priority.IMMEDIATE, + cacheStrategy = DiskCacheStrategy.NONE + ) + } + + binding.image.run { + shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius * 0.8F) + + if (photoClickListener != null) { + setOnClickListener { photoClickListener?.invoke(size.url) } + } else { + setOnClickListener(null) } - roundedLayout.addView(newPhoto) - container.addView(roundedLayout) - } else { - container.addView(newPhoto) + loadWithGlide( + url = size.url, + crossFade = true, + placeholderDrawable = ColorDrawable(colorBackground), + priority = Priority.LOW + ) } - - newPhoto.load(size.url) { crossfade(100) } } private fun video(video: VkVideo) { - val size = video.images[1] - - val layout = FrameLayout(context).apply { - layoutParams = LinearLayoutCompat.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - } - - val newPhoto = ShapeableImageView(context).apply { - layoutParams = FrameLayout.LayoutParams( - AndroidUtils.px(size.width).roundToInt(), - AndroidUtils.px(size.height).roundToInt() - ) - - shapeAppearanceModel = - shapeAppearanceModel.withCornerSize { AndroidUtils.px(5) } - scaleType = ImageView.ScaleType.CENTER_CROP - } - - val play = AppCompatImageView(context).apply { - layoutParams = FrameLayout.LayoutParams( - AndroidUtils.px(50).roundToInt(), - AndroidUtils.px(50).roundToInt() - ).also { - it.gravity = Gravity.CENTER - } - - backgroundTintList = ColorStateList.valueOf(playBackgroundColor) - imageTintList = ColorStateList.valueOf(playColor) - - setBackgroundResource(R.drawable.ic_play_button_circle_background) - setImageResource(R.drawable.ic_round_play_arrow_24) - - setPadding(12) - } - - layout.addView(newPhoto) - layout.addView(play) - val spacer = Space(context).apply { - layoutParams = LinearLayoutCompat.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - AndroidUtils.px(5).roundToInt() + 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 = 8.dpToPx().toFloat() + + binding.border.run { + shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius) + + updateLayoutParams { + width = (displayMetrics.widthPixels * widthMultiplier).roundToInt() + dimensionRatio = ratio + } + loadWithGlide( + drawable = ColorDrawable(colorSecondary), + priority = Priority.IMMEDIATE, + cacheStrategy = DiskCacheStrategy.NONE ) } - if (container.isNotEmpty()) - container.addView(spacer) + binding.image.run { + shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius * 0.8F) - container.addView(layout) - - newPhoto.load(size.url) { crossfade(100) } + loadWithGlide( + url = size.url, + crossFade = true, + placeholderDrawable = ColorDrawable(colorBackground), + priority = Priority.LOW + ) + } } private fun audio(audio: VkAudio) { @@ -245,14 +246,14 @@ class AttachmentInflater constructor( val binding = ItemMessageAttachmentLinkBinding.inflate(inflater, textContainer, true) binding.title.text = link.title - binding.title.isVisible = !link.title.isNullOrBlank() + binding.title.toggleVisibility(!link.title.isNullOrBlank()) binding.caption.text = link.caption - binding.caption.isVisible = !link.caption.isNullOrBlank() + binding.caption.toggleVisibility(!link.caption.isNullOrBlank()) - link.photo?.getSizeOrSmaller('y')?.let { - binding.preview.load(it.url) { crossfade(150) } - binding.linkIcon.isVisible = false + link.photo?.getSizeOrSmaller('y')?.let { size -> + binding.preview.loadWithGlide(url = size.url, crossFade = true) + binding.linkIcon.gone() return } @@ -264,7 +265,7 @@ class AttachmentInflater constructor( ) ) ) - binding.linkIcon.isVisible = true + binding.linkIcon.visible() } private fun sticker(sticker: VkSticker) { @@ -272,13 +273,12 @@ class AttachmentInflater constructor( val url = sticker.urlForSize(352) - with(binding.image) { - layoutParams = LinearLayoutCompat.LayoutParams( - AndroidUtils.px(140).roundToInt(), - AndroidUtils.px(140).roundToInt() - ) + binding.image.run { + val size = 140.dpToPx() - load(url) { crossfade(150) } + layoutParams = LinearLayoutCompat.LayoutParams(size, size) + + loadWithGlide(url = url, crossFade = true) } } @@ -307,14 +307,14 @@ class AttachmentInflater constructor( } binding.postTitle.text = context.getString(postTitleRes) - binding.postTitle.isVisible = false + binding.postTitle.gone() - binding.avatar.isVisible = group != null || user != null + binding.avatar.toggleVisibility(group != null || user != null) if (binding.avatar.isVisible) { - binding.avatar.load(avatar) { crossfade(150) } + binding.avatar.loadWithGlide(url = avatar, crossFade = true) } else { - binding.avatar.setImageDrawable(null) + binding.avatar.clear() } binding.title.text = title @@ -328,12 +328,13 @@ class AttachmentInflater constructor( private fun voice(voiceMessage: VkVoiceMessage) { val binding = ItemMessageAttachmentVoiceBinding.inflate(inflater, textContainer, true) - if (message.isOut) + if (message.isOut) { + val padding = 6.dpToPx() binding.root.updatePadding( - bottom = AndroidUtils.px(6).roundToInt(), - left = AndroidUtils.px(6).roundToInt() + bottom = padding, + left = padding ) - + } val waveform = IntArray(voiceMessage.waveform.size) voiceMessage.waveform.forEachIndexed { index, i -> waveform[index] = i } @@ -352,8 +353,8 @@ class AttachmentInflater constructor( if (message.isOut) binding.root.updatePadding( - bottom = AndroidUtils.px(5).roundToInt(), - left = AndroidUtils.px(6).roundToInt() + bottom = 5.dpToPx(), + left = 6.dpToPx() ) val callType = @@ -383,15 +384,17 @@ class AttachmentInflater constructor( val url = graffiti.url - val heightCoefficient = graffiti.height / AndroidUtils.px(140) + val size = 140.dpToPx() - with(binding.image) { + val heightCoefficient = graffiti.height / size.toFloat() + + binding.image.run { layoutParams = LinearLayoutCompat.LayoutParams( - AndroidUtils.px(140).roundToInt(), + size, (graffiti.height / heightCoefficient).roundToInt() ) - load(url) { crossfade(150) } + loadWithGlide(url = url, crossFade = true) } } @@ -400,16 +403,72 @@ class AttachmentInflater constructor( val url = gift.thumb256 ?: gift.thumb96 ?: gift.thumb48 - with(binding.image) { - shapeAppearanceModel = shapeAppearanceModel.withCornerSize { AndroidUtils.px(12) } + binding.image.run { + val size = 140.dpToPx() - layoutParams = LinearLayoutCompat.LayoutParams( - AndroidUtils.px(140).roundToInt(), - AndroidUtils.px(140).roundToInt() - ) + shapeAppearanceModel = shapeAppearanceModel.withCornerSize(12.dpToPx().toFloat()) - load(url) { crossfade(150) } + layoutParams = LinearLayoutCompat.LayoutParams(size, size) + + loadWithGlide(url = 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( + drawable = dimmerDrawable, + transformations = listOf(TypeTransformations.RoundedCornerCrop(cornersRadius)), + priority = Priority.IMMEDIATE, + cacheStrategy = DiskCacheStrategy.NONE + ) + + binding.image.run { + shapeAppearanceModel = shapeAppearanceModel.withCornerSize(cornersRadius.toFloat()) + + loadWithGlide( + url = 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) + } + } } \ No newline at end of file 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 index 038b4a25..f58ac1af 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt @@ -1,8 +1,10 @@ package com.meloda.fast.screens.messages +import android.annotation.SuppressLint import android.content.Context import android.graphics.Color import android.graphics.drawable.ColorDrawable +import android.util.Log import android.view.View import android.view.ViewGroup import android.widget.Toast @@ -19,76 +21,89 @@ import com.meloda.fast.api.model.VkUser import com.meloda.fast.api.model.attachments.VkPhoto import com.meloda.fast.base.adapter.BaseAdapter import com.meloda.fast.base.adapter.BaseHolder -import com.meloda.fast.databinding.* -import com.meloda.fast.util.AndroidUtils -import java.util.* -import kotlin.math.roundToInt +import com.meloda.fast.databinding.ItemMessageInBinding +import com.meloda.fast.databinding.ItemMessageOutBinding +import com.meloda.fast.databinding.ItemMessageServiceBinding +import com.meloda.fast.extensions.dpToPx +import com.meloda.fast.model.DataItem class MessagesHistoryAdapter constructor( context: Context, - values: MutableList, val conversation: VkConversation, val profiles: HashMap = hashMapOf(), val groups: HashMap = hashMapOf() -) : BaseAdapter(context, values, COMPARATOR) { +) : BaseAdapter, MessagesHistoryAdapter.BasicHolder>(context, Comparator) { var avatarLongClickListener: ((position: Int) -> Unit)? = null override fun getItemViewType(position: Int): Int { - when { - isPositionHeader(position) -> return HEADER - isPositionFooter(position) -> return FOOTER + return when (val item = getItem(position)) { + is VkMessage -> { + return when { + item.action != null -> TypeService + item.isOut -> TypeOutgoing + !item.isOut -> TypeIncoming + else -> -1 + } + } + is DataItem.Header -> { + return TypeHeader + } + is DataItem.Footer -> { + return TypeFooter + } + else -> -1 } - - getItem(position).let { message -> - if (message.action != null) return SERVICE - if (message.isOut) return OUTGOING - if (!message.isOut) return INCOMING - } - - return -1 } - private fun isPositionHeader(position: Int) = position == 0 - private fun isPositionFooter(position: Int) = position >= actualSize - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BasicHolder { return when (viewType) { // magick numbers is great! - HEADER -> Header(createEmptyView(60)) - FOOTER -> Footer(createEmptyView(36)) - SERVICE -> ServiceMessage( + TypeHeader -> { + Header(createEmptyView(60)) + } + TypeFooter -> { + Footer( + createEmptyView( + context.resources.getDimensionPixelSize(R.dimen.messages_history_input_panel_height_with_margins) + ) + ) + } + TypeService -> ServiceMessage( ItemMessageServiceBinding.inflate(inflater, parent, false) ) - OUTGOING -> OutgoingMessage( + TypeOutgoing -> OutgoingMessage( ItemMessageOutBinding.inflate(inflater, parent, false) ) - INCOMING -> IncomingMessage( + TypeIncoming -> IncomingMessage( ItemMessageInBinding.inflate(inflater, parent, false) ) else -> throw IllegalStateException("Wrong viewType: $viewType") } } -// override fun initListeners(itemView: View, position: Int) { -// if (itemView is AdapterView<*>) return -// -// itemView.setOnClickListener { onItemClickListener?.invoke(position, itemView) } -// itemView.setOnLongClickListener { itemLongClickListener.invoke(position) } -// } + override fun onBindViewHolder(holder: BasicHolder, position: Int) { + if (holder is Header || holder is Footer) { + Log.d( + "MessagesHistoryAdapter", + "onBindViewHolder: index $position, holder is ${holder.javaClass.simpleName}. Skip" + ) + return + } + Log.d( + "MessagesHistoryAdapter", + "onBindViewHolder: index $position, holder is ${holder.javaClass.simpleName}. Bind" + ) - val actualSize get() = values.size - - override fun getItemCount(): Int { - if (actualSize == 0) return 2 - return super.getItemCount() + 2 + initListeners(holder.itemView, position) + holder.bind(position) } private fun createEmptyView(size: Int) = View(context).apply { layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, - AndroidUtils.px(size).roundToInt() + size ) isEnabled = false @@ -96,13 +111,6 @@ class MessagesHistoryAdapter constructor( isFocusable = false } - override fun onBindViewHolder(holder: BasicHolder, position: Int) { - if (holder is Header || holder is Footer) return - - initListeners(holder.itemView, position) - holder.bind(position) - } - open inner class BasicHolder(v: View = View(context)) : BaseHolder(v) inner class Header(v: View) : BasicHolder(v) @@ -114,10 +122,10 @@ class MessagesHistoryAdapter constructor( ) : BasicHolder(binding.root) { override fun bind(position: Int) { - val message = getItem(position) + val message = getItem(position) as VkMessage - val prevMessage = getOrNull(position - 1) - val nextMessage = getOrNull(position + 1) + val prevMessage = getVkMessage(getOrNull(position - 1)) + val nextMessage = getVkMessage(getOrNull(position + 1)) MessagesPreparator( context = context, @@ -159,9 +167,8 @@ class MessagesHistoryAdapter constructor( ) : BasicHolder(binding.root) { override fun bind(position: Int) { - val message = getItem(position) - - val prevMessage = getOrNull(position - 1) + val message = getItem(position) as VkMessage + val prevMessage = getVkMessage(getOrNull(position - 1)) MessagesPreparator( context = context, @@ -192,13 +199,12 @@ class MessagesHistoryAdapter constructor( private val youPrefix = context.getString(R.string.you_message_prefix) init { - binding.photo.shapeAppearanceModel.run { - withCornerSize { AndroidUtils.px(4) } - } + binding.photo.shapeAppearanceModel = + binding.photo.shapeAppearanceModel.withCornerSize(4.dpToPx().toFloat()) } override fun bind(position: Int) { - val message = getItem(position) + val message = getItem(position) as VkMessage val messageUser = if (message.isUser()) profiles[message.fromId] @@ -241,59 +247,56 @@ class MessagesHistoryAdapter constructor( } } - fun removeMessageById(id: Int): Int? { - for (i in values.indices) { - val message = values[i] - if (message.id == id) { - values.removeAt(i) - return i - } + fun getVkMessage(item: DataItem<*>?): VkMessage? { + if (item == null) return null + if (item is VkMessage) return item + + return null + } + + 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 removeMessagesByIds(ids: List): List { - val positions = mutableListOf() - - for (i in values.indices) { - val message = values[i] - if (ids.contains(message.id)) { - values.removeAt(i) - positions += i - } - } - - return positions - } - - fun searchMessageIndex(messageId: Int): Int? { - for (i in values.indices) { - val message = values[i] - if (message.id == messageId) return i + 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 SERVICE = 1 - private const val HEADER = 0 - private const val FOOTER = 2 - private const val INCOMING = 3 - private const val OUTGOING = 4 + private const val TypeService = 1 + private const val TypeHeader = 0 + private const val TypeFooter = 2 + private const val TypeIncoming = 3 + private const val TypeOutgoing = 4 - - private val COMPARATOR = object : DiffUtil.ItemCallback() { + private val Comparator = object : DiffUtil.ItemCallback>() { override fun areItemsTheSame( - oldItem: VkMessage, - newItem: VkMessage - ) = false + oldItem: DataItem, + newItem: DataItem + ): Boolean { + return if (oldItem is VkMessage && newItem is VkMessage) { + oldItem.id == newItem.id + } else { + oldItem is DataItem.Footer && newItem is DataItem.Footer + || oldItem is DataItem.Header && newItem is DataItem.Header + } + } + @SuppressLint("DiffUtilEquals") override fun areContentsTheSame( - oldItem: VkMessage, - newItem: VkMessage - ) = false + oldItem: DataItem, + newItem: DataItem + ): Boolean = oldItem == newItem } } } \ No newline at end of file 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 index eea80a77..0d27f113 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt @@ -1,16 +1,21 @@ package com.meloda.fast.screens.messages +import android.animation.ValueAnimator import android.content.res.ColorStateList import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.text.TextUtils import android.view.View +import android.view.animation.LinearInterpolator import android.viewbinding.library.fragment.viewBinding import android.widget.Toast +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.core.view.setPadding +import androidx.core.view.updateLayoutParams import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.viewModels import androidx.lifecycle.MutableLiveData @@ -32,7 +37,9 @@ import com.meloda.fast.base.viewmodel.StopProgressEvent import com.meloda.fast.base.viewmodel.VkEvent import com.meloda.fast.databinding.DialogMessageDeleteBinding import com.meloda.fast.databinding.FragmentMessagesHistoryBinding -import com.meloda.fast.extensions.TextViewExtensions.clear +import com.meloda.fast.extensions.* +import com.meloda.fast.extensions.ImageLoader.clear +import com.meloda.fast.extensions.ImageLoader.loadWithGlide import com.meloda.fast.util.AndroidUtils import com.meloda.fast.util.TimeUtils import dagger.hilt.android.AndroidEntryPoint @@ -41,10 +48,26 @@ import java.util.* import kotlin.concurrent.schedule import kotlin.math.roundToInt + @AndroidEntryPoint class MessagesHistoryFragment : BaseViewModelFragment(R.layout.fragment_messages_history) { + 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(bundle: Bundle): MessagesHistoryFragment { + val fragment = MessagesHistoryFragment() + fragment.arguments = bundle + + return fragment + } + } + override val viewModel: MessagesHistoryViewModel by viewModels() private val binding: FragmentMessagesHistoryBinding by viewBinding() @@ -55,21 +78,20 @@ class MessagesHistoryFragment : } private val user: VkUser? by lazy { - requireArguments().getParcelable("user") + requireArguments().getParcelable(ARG_USER) } private val group: VkGroup? by lazy { - requireArguments().getParcelable("group") + requireArguments().getParcelable(ARG_GROUP) } private val conversation: VkConversation by lazy { - requireNotNull(requireArguments().getParcelable("conversation")) + requireNotNull(requireArguments().getParcelable(ARG_CONVERSATION)) } private val adapter: MessagesHistoryAdapter by lazy { - MessagesHistoryAdapter(requireContext(), mutableListOf(), conversation).also { + MessagesHistoryAdapter(requireContext(), conversation).also { it.itemClickListener = this::onItemClick - it.itemLongClickListener = this::onItemLongClick it.avatarLongClickListener = this::onAvatarLongClickListener } } @@ -90,6 +112,8 @@ class MessagesHistoryFragment : else -> null } + binding.back.setOnClickListener { requireActivity().onBackPressed() } + binding.title.ellipsize = TextUtils.TruncateAt.END binding.status.ellipsize = TextUtils.TruncateAt.END @@ -121,7 +145,7 @@ class MessagesHistoryFragment : binding.action.setOnClickListener { performAction() } - binding.recyclerView.addOnLayoutChangeListener { _, i, i2, i3, bottom, i5, i6, i7, oldBottom -> + binding.recyclerView.addOnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom -> if (bottom >= oldBottom) return@addOnLayoutChangeListener val lastVisiblePosition = (binding.recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition() @@ -138,8 +162,8 @@ class MessagesHistoryFragment : val firstPosition = (recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() - val message = adapter.getOrNull(firstPosition) - message?.let { + adapter.getOrNull(firstPosition)?.let { + if (it !is VkMessage) return binding.timestamp.isVisible = true val time = "${ @@ -158,7 +182,7 @@ class MessagesHistoryFragment : timestampTimer = Timer() timestampTimer?.schedule(2500) { - recyclerView.post { binding.timestamp.isVisible = false } + recyclerView.post { binding.timestamp.gone() } } } @@ -185,6 +209,8 @@ class MessagesHistoryFragment : .scaleY(1.25f) .setDuration(100) .withEndAction { + if (getView() == null) return@withEndAction + binding.action.animate() .scaleX(1f) .scaleY(1f) @@ -209,21 +235,37 @@ class MessagesHistoryFragment : } } - attachmentController.isPanelVisible.observe(viewLifecycleOwner) { - if (it) binding.message.setSelection(binding.message.text.toString().length) + attachmentController.isPanelVisible.observe(viewLifecycleOwner) { isVisible -> + if (isVisible) binding.message.setSelection(binding.message.text.toString().length) - val layoutParams = binding.refreshLayout.layoutParams as CoordinatorLayout.LayoutParams - layoutParams.bottomMargin = - if (it) (binding.attachmentPanel.height / 1.5).roundToInt() else 0 + val currentMargin = + (binding.refreshLayout.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin + + val newMargin = + if (isVisible) (binding.attachmentPanel.measuredHeight / 1.5).roundToInt() + else 0 + + ValueAnimator.ofInt(currentMargin, newMargin).apply { + duration = ATTACHMENT_PANEL_ANIMATION_DURATION + interpolator = LinearInterpolator() + + addUpdateListener { animator -> + if (getView() == null) return@addUpdateListener + val value = animator.animatedValue as Int + binding.refreshLayout.updateLayoutParams { + bottomMargin = value + } + } + }.start() } binding.attachmentPanel.setOnClickListener c@{ val message = attachmentController.message.value ?: return@c - val index = adapter.values.indexOf(message) + val index = adapter.indexOf(message) if (index == -1) return@c - binding.recyclerView.smoothScrollToPosition(index) + binding.recyclerView.scrollToPosition(index) } binding.dismissReply.setOnClickListener { @@ -232,6 +274,11 @@ class MessagesHistoryFragment : } } + @ColorInt + private fun getColor(@ColorRes resId: Int): Int { + return ContextCompat.getColor(requireContext(), resId) + } + private fun prepareAvatar() { val avatar = when { conversation.ownerId == VKConstants.FAST_GROUP_ID -> null @@ -241,46 +288,49 @@ class MessagesHistoryFragment : else -> null } - binding.avatar.isVisible = avatar != null + val colorOnPrimary = getColor(R.color.colorOnPrimary) + val colorUserAvatarAction = getColor(R.color.colorUserAvatarAction) + val colorOnUserAvatarAction = getColor(R.color.colorOnUserAvatarAction) + + val icLauncherColor = getColor(R.color.a1_500) + + binding.avatar.toggleVisibility(avatar != null) if (avatar == null) { - binding.avatarPlaceholder.isVisible = true + binding.avatarPlaceholder.visible() if (conversation.ownerId == VKConstants.FAST_GROUP_ID) { - binding.placeholderBack.setImageDrawable( - ColorDrawable( - ContextCompat.getColor(requireContext(), R.color.a1_400) - ) + binding.placeholderBack.loadWithGlide( + drawable = ColorDrawable(icLauncherColor), + transformations = ImageLoader.userAvatarTransformations ) binding.placeholder.imageTintList = - ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.a1_0)) + ColorStateList.valueOf(colorOnPrimary) binding.placeholder.setImageResource(R.drawable.ic_fast_logo) binding.placeholder.setPadding(18) } else { - binding.placeholderBack.setImageDrawable( - ColorDrawable( - ContextCompat.getColor(requireContext(), R.color.n1_50) - ) + binding.placeholderBack.loadWithGlide( + drawable = ColorDrawable(colorOnUserAvatarAction), + transformations = ImageLoader.userAvatarTransformations ) binding.placeholder.imageTintList = - ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.n2_500)) + ColorStateList.valueOf(colorUserAvatarAction) binding.placeholder.setImageResource(R.drawable.ic_account_circle_cut) binding.placeholder.setPadding(0) - binding.avatar.setImageDrawable(null) + binding.avatar.clear() } } else { binding.avatar.load(avatar) { crossfade(200) target { - binding.avatarPlaceholder.isVisible = false + binding.avatarPlaceholder.gone() binding.avatar.setImageDrawable(it) } } } - binding.phantomIcon.isVisible = conversation.isPhantom - binding.online.isVisible = user?.online == true - binding.pin.isVisible = conversation.isPinned + binding.phantomIcon.toggleVisibility(conversation.isPhantom) + binding.online.toggleVisibility(user?.online) } private fun performAction() { @@ -293,8 +343,10 @@ class MessagesHistoryFragment : val date = System.currentTimeMillis() + val messageIndex = adapter.lastPosition + val message = VkMessage( - id = -1, + id = Int.MAX_VALUE, text = messageText, isOut = true, peerId = conversation.id, @@ -304,10 +356,10 @@ class MessagesHistoryFragment : replyMessage = attachmentController.message.value ) - adapter.add(message) - adapter.notifyItemInserted(adapter.actualSize - 1) - binding.recyclerView.smoothScrollToPosition(adapter.lastPosition) - binding.message.clear() + adapter.add(message, beforeFooter = true, commitCallback = { + binding.recyclerView.scrollToPosition(adapter.lastPosition) + binding.message.clear() + }) val replyMessage = attachmentController.message.value attachmentController.message.value = null @@ -316,8 +368,13 @@ class MessagesHistoryFragment : peerId = conversation.id, message = messageText, randomId = 0, - replyTo = replyMessage?.id - ) { message.id = it } + replyTo = replyMessage?.id, + setId = { messageId -> + val messageToUpdate = adapter[messageIndex] as VkMessage + messageToUpdate.id = messageId + adapter[messageIndex] = messageToUpdate + } + ) } Action.EDIT -> { val message = attachmentController.message.value ?: return @@ -336,6 +393,7 @@ class MessagesHistoryFragment : Action.DELETE -> attachmentController.message.value?.let { showDeleteMessageDialog(it) } + else -> {} } } @@ -346,12 +404,12 @@ class MessagesHistoryFragment : is StartProgressEvent -> onProgressStarted() is StopProgressEvent -> onProgressStopped() - is MessagesMarkAsImportant -> markMessagesAsImportant(event) - is MessagesLoaded -> refreshMessages(event) - is MessagesPin -> conversation.pinnedMessage = event.message - is MessagesUnpin -> conversation.pinnedMessage = null - is MessagesDelete -> deleteMessages(event) - is MessagesEdit -> editMessage(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) } } @@ -395,26 +453,24 @@ class MessagesHistoryFragment : } } - private fun markMessagesAsImportant(event: MessagesMarkAsImportant) { + private fun markMessagesAsImportant(event: MessagesMarkAsImportantEvent) { var changed = false val positions = mutableListOf() - for (i in adapter.values.indices) { - val message = adapter.values[i] + for (i in adapter.indices) { + val message = adapter[i] as VkMessage message.important = event.important if (event.messagesIds.contains(message.id)) { if (!changed) changed = true positions.add(i) - adapter.values[i] = message + adapter[i] = message } } - - if (changed) positions.forEach { adapter.notifyItemChanged(it) } } - private fun refreshMessages(event: MessagesLoaded) { + private fun refreshMessages(event: MessagesLoadedEvent) { adapter.profiles += event.profiles adapter.groups += event.groups @@ -424,22 +480,23 @@ class MessagesHistoryFragment : private fun fillRecyclerView(values: List) { val smoothScroll = adapter.isNotEmpty() - adapter.values.clear() - adapter.values += values.sortedBy { it.date } - adapter.notifyItemRangeChanged(0, adapter.itemCount) - - if (smoothScroll) binding.recyclerView.smoothScrollToPosition(adapter.lastPosition) - else binding.recyclerView.scrollToPosition(adapter.lastPosition) + adapter.setItems( + values.sortedBy { it.date }, + withHeader = true, + withFooter = true, + commitCallback = { + if (smoothScroll) binding.recyclerView.smoothScrollToPosition(adapter.lastPosition) + else binding.recyclerView.scrollToPosition(adapter.lastPosition) + } + ) } private fun onItemClick(position: Int) { showOptionsDialog(position) } - private fun onItemLongClick(position: Int) = true - private fun onAvatarLongClickListener(position: Int) { - val message = adapter.values[position] + val message = adapter[position] as VkMessage val messageUser = VkUtils.getMessageUser(message, adapter.profiles) val messageGroup = VkUtils.getMessageGroup(message, adapter.groups) @@ -449,7 +506,7 @@ class MessagesHistoryFragment : } private fun showOptionsDialog(position: Int) { - val message = adapter.values[position] + val message = adapter[position] as VkMessage if (message.action != null) return val time = getString( @@ -577,16 +634,14 @@ class MessagesHistoryFragment : .show() } - private fun deleteMessages(event: MessagesDelete) { - adapter.removeMessagesByIds(event.messagesIds).let { - it.forEach { index -> adapter.notifyItemRemoved(index) } - } + private fun deleteMessages(event: MessagesDeleteEvent) { + val messagesToDelete = event.messagesIds.mapNotNull { id -> adapter.searchMessageById(id) } + adapter.removeAll(messagesToDelete) } - private fun editMessage(event: MessagesEdit) { + private fun editMessage(event: MessagesEditEvent) { adapter.searchMessageIndex(event.message.id)?.let { index -> - adapter.values[index] = event.message - adapter.notifyItemChanged(index) + adapter[index] = event.message } } @@ -610,8 +665,6 @@ class MessagesHistoryFragment : } private fun applyMessage(message: VkMessage) { - showPanel() - val title = when { message.isGroup() && message.group.value != null -> message.group.value?.name message.isUser() && message.user.value != null -> message.user.value?.fullName @@ -637,6 +690,8 @@ class MessagesHistoryFragment : if (isEditing) { binding.message.setText(message.text) } + + showPanel() } private fun clearMessage() { @@ -651,28 +706,64 @@ class MessagesHistoryFragment : } } - private fun showPanel(duration: Long = 250) { + private fun showPanel() { + binding.attachmentPanel.visible() + binding.attachmentPanel.measure( + View.MeasureSpec.AT_MOST, View.MeasureSpec.UNSPECIFIED + ) + if (attachmentController.isPanelVisible.value == false) attachmentController.isPanelVisible.value = true + val measuredHeight = binding.attachmentPanel.measuredHeight + + binding.attachmentPanel.updateLayoutParams { + height = 0 + } + binding.attachmentPanel.animate() .translationY(0f) - .alpha(1f) - .setDuration(duration) - .withStartAction { binding.attachmentPanel.isVisible = true } + .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 + binding.attachmentPanel.updateLayoutParams { + height = value + } + } + }.start() } - private fun hidePanel(duration: Long = 250) { + private fun hidePanel() { if (attachmentController.isPanelVisible.value == true) attachmentController.isPanelVisible.value = false + val currentHeight = binding.attachmentPanel.height + binding.attachmentPanel.animate() - .alpha(0f) - .translationY(50f) - .setDuration(duration) - .withEndAction { binding.attachmentPanel.isVisible = false } + .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 + + binding.attachmentPanel.updateLayoutParams { + height = value + } + } + }.start() } } 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 index a4c07e50..e829cbc7 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt @@ -1,6 +1,8 @@ package com.meloda.fast.screens.messages import androidx.lifecycle.viewModelScope +import com.meloda.fast.api.LongPollEvent +import com.meloda.fast.api.LongPollUpdatesParser import com.meloda.fast.api.VKConstants import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkGroup @@ -16,12 +18,25 @@ import javax.inject.Inject @HiltViewModel class MessagesHistoryViewModel @Inject constructor( - private val messages: MessagesDataSource + private val messages: MessagesDataSource, + updatesParser: LongPollUpdatesParser ) : BaseViewModel() { - fun loadHistory( - peerId: Int - ) = viewModelScope.launch { + init { + updatesParser.onNewMessage { +// viewModelScope.launch { handleNewMessage(it) } + } + + updatesParser.onMessageEdited { + viewModelScope.launch { handleEditedMessage(it) } + } + } + + private suspend fun handleEditedMessage(event: LongPollEvent.VkMessageEditEvent) { + sendEvent(com.meloda.fast.screens.messages.MessagesEditEvent(event.message)) + } + + fun loadHistory(peerId: Int) = viewModelScope.launch { makeJob({ messages.getHistory( MessagesGetHistoryRequest( @@ -66,7 +81,7 @@ class MessagesHistoryViewModel @Inject constructor( } sendEvent( - MessagesLoaded( + MessagesLoadedEvent( count = response.count, profiles = profiles, groups = groups, @@ -116,7 +131,7 @@ class MessagesHistoryViewModel @Inject constructor( onAnswer = { val response = it.response ?: return@makeJob sendEvent( - MessagesMarkAsImportant( + MessagesMarkAsImportantEvent( messagesIds = response, important = important ) @@ -142,14 +157,14 @@ class MessagesHistoryViewModel @Inject constructor( }, onAnswer = { val response = it.response ?: return@makeJob - sendEvent(MessagesPin(response.asVkMessage())) + sendEvent(MessagesPinEvent(response.asVkMessage())) } ) } else { makeJob({ messages.unpin(MessagesUnPinMessageRequest(peerId = peerId)) }, onAnswer = { println("Fast::MessagesHistoryViewModel::unPin::Response::${it.response}") - sendEvent(MessagesUnpin) + sendEvent(MessagesUnpinEvent) } ) } @@ -172,7 +187,7 @@ class MessagesHistoryViewModel @Inject constructor( deleteForAll = deleteForAll ) ) - }, onAnswer = { sendEvent(MessagesDelete(messagesIds = messagesIds ?: listOf())) }) + }, onAnswer = { sendEvent(MessagesDeleteEvent(messagesIds = messagesIds ?: emptyList())) }) } fun editMessage( @@ -195,13 +210,13 @@ class MessagesHistoryViewModel @Inject constructor( }, onAnswer = { originalMessage.text = message - sendEvent(MessagesEdit(originalMessage)) + sendEvent(com.meloda.fast.screens.messages.MessagesEditEvent(originalMessage)) } ) } } -data class MessagesLoaded( +data class MessagesLoadedEvent( val count: Int, val conversations: HashMap, val messages: List, @@ -209,21 +224,12 @@ data class MessagesLoaded( val groups: HashMap ) : VkEvent() -data class MessagesMarkAsImportant( - val messagesIds: List, - val important: Boolean -) : VkEvent() +data class MessagesMarkAsImportantEvent(val messagesIds: List, val important: Boolean) : VkEvent() -data class MessagesPin( - val message: VkMessage -) : VkEvent() +data class MessagesPinEvent(val message: VkMessage) : VkEvent() -object MessagesUnpin : VkEvent() +object MessagesUnpinEvent : VkEvent() -data class MessagesDelete( - val messagesIds: List -) : VkEvent() +data class MessagesDeleteEvent(val messagesIds: List) : VkEvent() -data class MessagesEdit( - val message: VkMessage -) : VkEvent() \ No newline at end of file +data class MessagesEditEvent(val message: VkMessage) : VkEvent() \ No newline at end of file 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 index b5bfc38e..a906f62d 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt @@ -12,6 +12,7 @@ import androidx.core.content.ContextCompat import androidx.core.view.isVisible import coil.load import com.meloda.fast.R +import com.meloda.fast.api.VKConstants import com.meloda.fast.api.VkUtils import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkGroup @@ -19,6 +20,9 @@ import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkUser import com.meloda.fast.api.model.attachments.VkSticker import com.meloda.fast.common.AppGlobal +import com.meloda.fast.extensions.gone +import com.meloda.fast.extensions.toggleVisibility +import com.meloda.fast.extensions.visible import com.meloda.fast.widget.BoundedLinearLayout import java.text.SimpleDateFormat import java.util.* @@ -150,9 +154,7 @@ class MessagesPreparator constructor( } private fun prepareUnreadIndicator() { - if (unread != null) { - unread.isVisible = message.isRead(conversation) - } + unread?.toggleVisibility(!message.isRead(conversation)) } private fun prepareSpacer() { @@ -160,12 +162,20 @@ class MessagesPreparator constructor( } private fun prepareAttachments() { + attachmentContainer?.removeAllViews() + + textContainer?.let { textContainer -> + if (textContainer.childCount > 1) { + textContainer.removeViews(1, textContainer.childCount - 1) + } + } + if (attachmentContainer != null && textContainer != null) { + if (message.attachments.isNullOrEmpty()) { - attachmentContainer.isVisible = false - attachmentContainer.removeAllViews() + attachmentContainer.gone() } else { - attachmentContainer.isVisible = true + attachmentContainer.visible() AttachmentInflater( context = context, @@ -208,11 +218,23 @@ class MessagesPreparator constructor( private fun prepareText() { if (bubble != null && text != null) { if (message.text == null) { - text.isVisible = false - bubble.isVisible = !message.attachments.isNullOrEmpty() + text.gone() + + val hasAttachments = !message.attachments.isNullOrEmpty() + var shouldBeVisible = hasAttachments + if (hasAttachments) { + for (attachment in message.attachments ?: emptyList()) { + if (VKConstants.separatedFromTextAttachments.contains(attachment.javaClass)) { + shouldBeVisible = false + break + } + } + } + + bubble.toggleVisibility(shouldBeVisible) } else { - text.isVisible = true - bubble.isVisible = true + text.visible() + bubble.visible() text.text = VkUtils.prepareMessageText(message.text ?: "") } } 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 45d9e807..00000000 --- a/app/src/main/kotlin/com/meloda/fast/service/LongPollService.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.meloda.fast.service - -import android.util.Log -import com.meloda.fast.api.network.messages.MessagesGetLongPollServerRequest -import com.meloda.fast.api.network.messages.MessagesDataSource -import com.meloda.fast.api.network.longpoll.LongPollRepo -import kotlinx.coroutines.* -import javax.inject.Inject -import kotlin.coroutines.CoroutineContext - -class LongPollService { -} - -class LongPollTask @Inject constructor( - private val dataSource: MessagesDataSource, - private val longPollRepo: LongPollRepo -) : CoroutineScope { - - companion object { - const val TAG = "LongPollTask" - } - - private val job = SupervisorJob() - - private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> - Log.d(TAG, "error: $throwable") - throwable.printStackTrace() - } - - override val coroutineContext: CoroutineContext - get() = Dispatchers.Default + job + exceptionHandler - - fun startPolling(): Job { - if (job.isCompleted || job.isCancelled) throw Exception("Job is over") - - return launch { - val serverInfo = dataSource.getLongPollServer( - MessagesGetLongPollServerRequest( - needPts = true, - version = 10 - ) - ) - - println("TESTJOPAAAAAA: $serverInfo") -// val response = serverInfo.response ?: return@launch - - - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/service/MessagesUpdateService.kt b/app/src/main/kotlin/com/meloda/fast/service/MessagesUpdateService.kt new file mode 100644 index 00000000..a7a5657c --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/service/MessagesUpdateService.kt @@ -0,0 +1,182 @@ +package com.meloda.fast.service + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import android.util.Log +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.meloda.fast.api.LongPollUpdatesParser +import com.meloda.fast.api.VKConstants +import com.meloda.fast.api.VKException +import com.meloda.fast.api.model.base.BaseVkLongPoll +import com.meloda.fast.api.network.Answer +import com.meloda.fast.api.network.longpoll.LongPollGetUpdatesRequest +import com.meloda.fast.api.network.longpoll.LongPollRepo +import com.meloda.fast.api.network.messages.MessagesDataSource +import com.meloda.fast.api.network.messages.MessagesGetLongPollServerRequest +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.* +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext + +@AndroidEntryPoint +class MessagesUpdateService : Service(), CoroutineScope { + + companion object { + const val TAG = "LongPollTask" + } + + private val job = SupervisorJob() + + private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + Log.d(TAG, "error: $throwable") + throwable.printStackTrace() + } + + override val coroutineContext: CoroutineContext + get() = Dispatchers.Default + job + exceptionHandler + + @Inject + lateinit var dataSource: MessagesDataSource + + @Inject + lateinit var longPollRepo: LongPollRepo + + @Inject + lateinit var updatesParser: LongPollUpdatesParser + + override fun onBind(p0: Intent?): IBinder? { + return null + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + launch { startPolling().join() } + return START_STICKY + } + + private fun startPolling(): Job { + if (job.isCompleted || job.isCancelled) throw Exception("Job is over") + + return launch { + var serverInfo = getServerInfo() + ?: throw VKException(error = "bad VK response (server info)") + + var lastUpdatesResponse: JsonObject? = getUpdatesResponse(serverInfo) + ?: throw VKException(error = "initiation error: bad VK response (last updates)") + + var failCount = 0 + + while (job.isActive) { + if (lastUpdatesResponse == null) { + failCount++ + serverInfo = getServerInfo() + ?: throw VKException(error = "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 VKException( + error = "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 = dataSource.getLongPollServer( + MessagesGetLongPollServerRequest( + needPts = true, + version = VKConstants.LP_VERSION + ) + ) + + println("$TAG: serverInfoResponse: $response") + + if (response is Answer.Error) return null + if (response is Answer.Success) { + return response.data.response + } + + return null + } + + private suspend fun getUpdatesResponse(server: BaseVkLongPoll): JsonObject? { + val response = dataSource.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 + ) + ) + + println("$TAG: lastUpdateResponse: $response") + + if (response is Answer.Error) return null + + if (response is Answer.Success) { + return response.data + } + + return null + } + + private fun handleUpdateEvent(eventJson: JsonArray) { +// println("$TAG: handleUpdateEvent: $eventJson") + + updatesParser.parseNextUpdate(eventJson) + } + +// fun registerListener(eventType: Int, listener: VkEventCallback) = +// updatesParser.registerListener(eventType, listener) + + override fun onDestroy() { + try { + job.cancel() + } catch (e: Exception) { + } + updatesParser.clearListeners() + super.onDestroy() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/service/OnlineService.kt b/app/src/main/kotlin/com/meloda/fast/service/OnlineService.kt new file mode 100644 index 00000000..f654dc84 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/service/OnlineService.kt @@ -0,0 +1,75 @@ +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.AccountDataSource +import com.meloda.fast.api.network.account.AccountSetOfflineRequest +import com.meloda.fast.api.network.account.AccountSetOnlineRequest +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.* +import java.util.* +import javax.inject.Inject +import kotlin.concurrent.schedule +import kotlin.coroutines.CoroutineContext + +@AndroidEntryPoint +class OnlineService : Service(), CoroutineScope { + + private companion object { + private const val TAG = "OnlineService" + } + + private val job = SupervisorJob() + + private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + Log.d(MessagesUpdateService.TAG, "error: $throwable") + throwable.printStackTrace() + } + + override val coroutineContext: CoroutineContext + get() = Dispatchers.Default + job + exceptionHandler + + @Inject + lateinit var dataSource: AccountDataSource + + private var timer: Timer? = null + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + timer = Timer().apply { + schedule(delay = 0, period = 60_000) { + launch { + setOffline() + delay(5000) + setOnline() + } + } + } + + return START_STICKY_COMPATIBILITY + } + + private suspend fun setOnline() { + println("$TAG: setOnline()") + val response = dataSource.setOnline( + AccountSetOnlineRequest( + voip = false, + accessToken = UserConfig.fastToken + ) + ) + } + + private suspend fun setOffline() { + println("$TAG: setOffline()") + val response = dataSource.setOffline( + AccountSetOfflineRequest( + accessToken = UserConfig.accessToken + ) + ) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/util/AndroidUtils.kt b/app/src/main/kotlin/com/meloda/fast/util/AndroidUtils.kt index aab07505..f9a04c30 100644 --- a/app/src/main/kotlin/com/meloda/fast/util/AndroidUtils.kt +++ b/app/src/main/kotlin/com/meloda/fast/util/AndroidUtils.kt @@ -4,7 +4,6 @@ import android.content.ClipData import android.content.Context import android.content.res.Configuration import android.net.NetworkCapabilities -import android.util.DisplayMetrics import android.util.TypedValue import androidx.annotation.AttrRes import com.meloda.fast.common.AppGlobal @@ -12,22 +11,8 @@ import com.meloda.fast.common.AppGlobal object AndroidUtils { - fun px(dp: Float): Float { - return dp * (AppGlobal.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT) - } - - fun px(dp: Int) = px(dp.toFloat()) - - fun dp(px: Float): Float { - return px / (AppGlobal.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT) - } - - fun dp(px: Int) = dp(px.toFloat()) - fun isDarkTheme(): Boolean { - val currentNightMode = - AppGlobal.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK - return when (currentNightMode) { + return when (AppGlobal.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { Configuration.UI_MODE_NIGHT_YES -> true else -> false } diff --git a/app/src/main/kotlin/com/meloda/fast/widget/NoItemsView.kt b/app/src/main/kotlin/com/meloda/fast/widget/NoItemsView.kt index f6770643..94caf9a0 100644 --- a/app/src/main/kotlin/com/meloda/fast/widget/NoItemsView.kt +++ b/app/src/main/kotlin/com/meloda/fast/widget/NoItemsView.kt @@ -13,8 +13,7 @@ import androidx.annotation.StringRes import androidx.appcompat.content.res.AppCompatResources import androidx.core.view.isVisible import com.meloda.fast.R -import com.meloda.fast.util.AndroidUtils -import kotlin.math.roundToInt +import com.meloda.fast.extensions.dpToPx @Suppress("UNCHECKED_CAST") class NoItemsView @JvmOverloads constructor( @@ -43,7 +42,7 @@ class NoItemsView @JvmOverloads constructor( private fun create() { val a = context.obtainStyledAttributes(attrs, R.styleable.NoItemsView) - minimumWidth = AndroidUtils.px(256).roundToInt() + minimumWidth = 256.dpToPx() minimumHeight = minimumWidth orientation = VERTICAL @@ -51,9 +50,12 @@ class NoItemsView @JvmOverloads constructor( noItemsPicture = ImageView(context) - val params = imageViewParams - params.height = AndroidUtils.px(64).roundToInt() - params.width = AndroidUtils.px(64).roundToInt() + val imageViewSize = 64.dpToPx() + + val params = imageViewParams.apply { + height = imageViewSize + width = imageViewSize + } noItemsPicture.layoutParams = params @@ -72,10 +74,10 @@ class NoItemsView @JvmOverloads constructor( noItemsTextView = TextView(context) val textParams = textViewParams - textParams.width = AndroidUtils.px(256).roundToInt() + textParams.width = 256.dpToPx() if (noItemsDrawable != null) { - textParams.topMargin = AndroidUtils.px(8).roundToInt() + textParams.topMargin = 8.dpToPx() } noItemsTextView.layoutParams = textParams 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 new file mode 100644 index 00000000..35af533c --- /dev/null +++ b/app/src/main/res/drawable/ic_message_attachment_story_image_dimmer.xml @@ -0,0 +1,10 @@ + + + + + + \ 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 index d2e74ac8..31e9209f 100644 --- a/app/src/main/res/drawable/ic_message_out_background.xml +++ b/app/src/main/res/drawable/ic_message_out_background.xml @@ -4,9 +4,9 @@ + android:color="?colorSurfaceVariant" /> - + + android:color="?colorSurfaceVariant" /> - + - + - + + android:color="?colorBackground" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_round_arrow_back_24.xml b/app/src/main/res/drawable/ic_round_arrow_back_24.xml new file mode 100644 index 00000000..6182502c --- /dev/null +++ b/app/src/main/res/drawable/ic_round_arrow_back_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 395fd0e6..124de6fa 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,21 +1,5 @@ - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_captcha.xml b/app/src/main/res/layout/dialog_captcha.xml index 3bd72f72..31348ebd 100644 --- a/app/src/main/res/layout/dialog_captcha.xml +++ b/app/src/main/res/layout/dialog_captcha.xml @@ -1,84 +1,78 @@ - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="@dimen/activity_horizontal_margin"> + + + + + + + + + + + + + + + + android:gravity="center" + android:orientation="horizontal"> - - - + android:layout_marginEnd="@dimen/activity_horizontal_margin" + android:layout_weight="1" + android:backgroundTint="@color/a1_600" + android:text="@android:string/cancel" + app:elevation="0dp" /> - - - - - - - - - - - - - - - - - - - - + android:layout_weight="1" + android:backgroundTint="@color/a3_200" + android:text="@android:string/ok" + app:elevation="0dp" /> - \ No newline at end of file + \ 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 index f5ba3f90..a021c392 100644 --- a/app/src/main/res/layout/dialog_message_delete.xml +++ b/app/src/main/res/layout/dialog_message_delete.xml @@ -1,22 +1,17 @@ - + - - - - - - - \ No newline at end of file + android:paddingStart="12dp" + android:paddingEnd="12dp" + android:text="@string/message_delete_for_all" + app:useMaterialThemeColors="true" /> + \ 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 index eada421a..9bf823a9 100644 --- a/app/src/main/res/layout/dialog_validation.xml +++ b/app/src/main/res/layout/dialog_validation.xml @@ -1,77 +1,71 @@ - + + + + + + + + + + + + + + + android:gravity="center" + android:orientation="horizontal"> - + android:layout_marginEnd="@dimen/activity_horizontal_margin" + android:layout_weight="1" + android:backgroundTint="@color/n1_900" + android:text="@android:string/cancel" + app:elevation="0dp" /> - - - - - - - - - - - - - - - - - - - - + android:layout_weight="1" + android:backgroundTint="@color/a3_200" + android:text="@android:string/ok" + app:elevation="0dp" /> - \ No newline at end of file + \ 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 index 0a66795e..59042084 100644 --- a/app/src/main/res/layout/fragment_conversations.xml +++ b/app/src/main/res/layout/fragment_conversations.xml @@ -1,142 +1,92 @@ - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> - + android:layout_height="wrap_content" + app:elevation="0dp"> - + android:layout_height="?actionBarSize" + android:background="?colorBackground" + android:elevation="0dp" + app:layout_scrollFlags="scroll|enterAlways|snap" + app:menu="@menu/fragment_conversations" + app:title="@string/title_messages" + app:titleTextColor="?colorOnBackground"> - + android:layout_gravity="end|center_vertical" + android:orientation="horizontal" + android:paddingStart="0dp" + android:paddingEnd="16dp" + app:layout_collapseMode="none"> - + - + - + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:orientation="vertical" + android:scrollbars="vertical" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:listitem="@layout/item_conversation" /> - + - + - - - - - - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml index b4f85e10..4b17b183 100644 --- a/app/src/main/res/layout/fragment_login.xml +++ b/app/src/main/res/layout/fragment_login.xml @@ -1,148 +1,141 @@ - + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/loginRoot" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> - + android:visibility="gone" /> - + - + android:layout_height="wrap_content" + android:animateLayoutChanges="true" + android:gravity="center" + android:orientation="vertical" + android:padding="16dp"> + + + + + + + + android:layout_marginTop="48dp" + android:orientation="horizontal"> - - - - - - + android:layout_marginTop="16dp" + android:src="@drawable/ic_baseline_account_circle_24" + app:tint="?colorAccent" /> - + app:boxStrokeErrorColor="@android:color/transparent"> - - - + android:layout_height="48dp" + android:hint="@string/login_hint" + android:imeOptions="actionGo" + android:inputType="textEmailAddress" /> - - - - - - - - - - - - - - - - - - - + - - - \ No newline at end of file + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml deleted file mode 100644 index 6bd3be14..00000000 --- a/app/src/main/res/layout/fragment_main.xml +++ /dev/null @@ -1,27 +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 index 08338bec..c157381a 100644 --- a/app/src/main/res/layout/fragment_messages_history.xml +++ b/app/src/main/res/layout/fragment_messages_history.xml @@ -1,339 +1,328 @@ - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> - + android:layout_height="match_parent" + android:layout_marginTop="86dp"> - + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:itemCount="100" + tools:listitem="@layout/item_message_out" /> - + - + - + android:orientation="horizontal" + android:paddingStart="12dp" + android:paddingTop="18dp" + android:paddingEnd="30dp" + android:paddingBottom="24dp"> - + + + + + + android:id="@+id/avatarPlaceholder" + android:layout_width="match_parent" + android:layout_height="match_parent"> - + android:layout_margin="1dp" + tools:src="@color/colorOnUserAvatarAction" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:src="@drawable/ic_account_circle_cut" + app:tint="@color/colorUserAvatarAction" /> - + - + - + - + - + - + - + - + + + + - - - - - - - - + android:textColor="?colorOnBackground" + android:textSize="20sp" + tools:text="@tools:sample/full_names" /> + + - + + + + + + android:orientation="vertical"> - + - + + + + + + - - + android:ellipsize="end" + android:maxLines="1" + android:textColor="?colorOnBackground" + android:textSize="16sp" + app:fontFamily="@font/roboto_regular" + tools:text="Short Message." /> - + + + + + + + + + android:layout_marginTop="18dp" + android:layout_marginEnd="18dp" + android:background="?selectableItemBackgroundBorderless" + android:src="@drawable/ic_baseline_attach_file_24" + android:tint="?colorPrimary" /> - + - \ No newline at end of file + + + + \ 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 index b0a440ce..5ac5b951 100644 --- a/app/src/main/res/layout/item_conversation.xml +++ b/app/src/main/res/layout/item_conversation.xml @@ -1,255 +1,253 @@ - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginVertical="4dp" + android:orientation="horizontal"> - + android:layout_marginStart="20dp" + android:backgroundTint="?colorBackgroundVariant" + android:orientation="horizontal" + android:paddingVertical="8dp" + android:paddingStart="8dp" + android:paddingEnd="32dp" + tools:background="@drawable/ic_message_unread"> - + + + + android:id="@+id/avatarPlaceholder" + android:layout_width="match_parent" + android:layout_height="match_parent"> - + android:layout_margin="1dp" + tools:src="@color/colorOnUserAvatarAction" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:src="@drawable/ic_account_circle_cut" + app:tint="@color/colorUserAvatarAction" /> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + android:layout_height="wrap_content"> - - + android:textColor="?colorOnBackground" + android:textSize="20sp" + tools:text="Title" /> + + + + + + + + + + + + + + + + - - - - \ No newline at end of file + + \ 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 index 39d34689..ff0b9ec2 100644 --- a/app/src/main/res/layout/item_message_attachment_audio.xml +++ b/app/src/main/res/layout/item_message_attachment_audio.xml @@ -1,59 +1,53 @@ - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="4dp"> + + + + + + + android:layout_marginStart="8dp" + android:orientation="vertical"> - - - - - - - - - - - - - + android:ellipsize="end" + android:fontFamily="@font/google_sans_regular" + android:maxLines="1" + android:textColor="?colorOnBackground" + android:textSize="18sp" + tools:text="Даня, дай Фаст" /> + - - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_message_attachment_call.xml b/app/src/main/res/layout/item_message_attachment_call.xml index 2556aec9..843c0078 100644 --- a/app/src/main/res/layout/item_message_attachment_call.xml +++ b/app/src/main/res/layout/item_message_attachment_call.xml @@ -1,59 +1,54 @@ - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="4dp"> + + + + + + + android:layout_marginHorizontal="8dp" + android:orientation="vertical"> - - - - - - - + android:ellipsize="end" + android:fontFamily="@font/google_sans_regular" + android:maxLines="1" + android:textColor="?colorOnBackground" + android:textSize="18sp" + tools:text="Исходящий звонок" /> - - - - - + - - \ No newline at end of file + \ 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 index 74117d53..cadbb2a9 100644 --- a/app/src/main/res/layout/item_message_attachment_file.xml +++ b/app/src/main/res/layout/item_message_attachment_file.xml @@ -1,59 +1,53 @@ - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="4dp"> + + + + + + + android:layout_marginStart="8dp" + android:orientation="vertical"> - - - - - - - - - - - - - + android:ellipsize="end" + android:fontFamily="@font/google_sans_regular" + android:maxLines="1" + android:textColor="?colorOnBackground" + android:textSize="18sp" + tools:text="Kids" /> + - - \ No newline at end of file + \ 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 index 5cc4fe57..56adcaca 100644 --- a/app/src/main/res/layout/item_message_attachment_gift.xml +++ b/app/src/main/res/layout/item_message_attachment_gift.xml @@ -1,16 +1,11 @@ - + - - - - - - - \ No newline at end of file + android:layout_height="wrap_content" /> + \ 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 index 5cc4fe57..56adcaca 100644 --- a/app/src/main/res/layout/item_message_attachment_graffiti.xml +++ b/app/src/main/res/layout/item_message_attachment_graffiti.xml @@ -1,16 +1,11 @@ - + - - - - - - - \ No newline at end of file + android:layout_height="wrap_content" /> + \ 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 index d39cf8b5..c97a1d4b 100644 --- a/app/src/main/res/layout/item_message_attachment_link.xml +++ b/app/src/main/res/layout/item_message_attachment_link.xml @@ -1,65 +1,60 @@ - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="6dp"> + + + + + + + + + android:layout_height="match_parent" + android:layout_marginHorizontal="8dp" + android:gravity="center_vertical" + android:orientation="vertical"> - - - - - - - - - - - - - - - + android:ellipsize="end" + android:maxLines="1" + android:textColor="?colorOnBackground" + android:textSize="18sp" + app:fontFamily="@font/google_sans_regular" + tools:text="melod1n" /> + - - \ No newline at end of file + \ 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 new file mode 100644 index 00000000..0a6ce835 --- /dev/null +++ b/app/src/main/res/layout/item_message_attachment_photo.xml @@ -0,0 +1,32 @@ + + + + + + + + \ 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 index 5cc4fe57..56adcaca 100644 --- a/app/src/main/res/layout/item_message_attachment_sticker.xml +++ b/app/src/main/res/layout/item_message_attachment_sticker.xml @@ -1,16 +1,11 @@ - + - - - - - - - \ No newline at end of file + android:layout_height="wrap_content" /> + \ 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 new file mode 100644 index 00000000..7698f47e --- /dev/null +++ b/app/src/main/res/layout/item_message_attachment_story.xml @@ -0,0 +1,52 @@ + + + + + + + + + + \ 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 new file mode 100644 index 00000000..580a09d6 --- /dev/null +++ b/app/src/main/res/layout/item_message_attachment_video.xml @@ -0,0 +1,46 @@ + + + + + + + + + + \ 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 index 2fd7d1f9..774b8789 100644 --- a/app/src/main/res/layout/item_message_attachment_voice.xml +++ b/app/src/main/res/layout/item_message_attachment_voice.xml @@ -1,57 +1,54 @@ - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="4dp"> - + - - - - - - - - - + android:layout_gravity="center" + android:src="@drawable/ic_round_play_arrow_24" + app:tint="@color/a3_700" /> - + - \ No newline at end of file + + + + \ 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 index 130a0312..056f58e1 100644 --- a/app/src/main/res/layout/item_message_attachment_wall_post.xml +++ b/app/src/main/res/layout/item_message_attachment_wall_post.xml @@ -1,69 +1,61 @@ - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="6dp"> + + + android:orientation="horizontal"> - + + android:layout_marginHorizontal="8dp" + android:orientation="vertical"> - - - - - - - - - - + android:ellipsize="end" + android:maxLines="1" + android:textColor="?colorOnBackground" + android:textSize="18sp" + app:fontFamily="@font/google_sans_regular" + tools:text="Typical Programmer" /> + - - - \ No newline at end of file + \ 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 index 83f5bc99..332e3c67 100644 --- a/app/src/main/res/layout/item_message_in.xml +++ b/app/src/main/res/layout/item_message_in.xml @@ -1,102 +1,100 @@ - + + + + android:orientation="vertical"> - + + + + android:gravity="bottom" + android:orientation="horizontal"> - - - + android:background="@drawable/ic_message_in_background" + android:backgroundTint="?colorSurfaceVariant" + android:minWidth="60dp" + android:orientation="vertical" + tools:ignore="UselessParent"> - - - + android:orientation="vertical"> - + android:layout_gravity="center_vertical" + android:autoLink="all" + android:padding="15dp" + android:textColor="?colorOnBackground" + tools:text="This" /> - + + - - - - - - - - - - + + + + + + - \ No newline at end of file + \ 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 index 3b4da87e..ca88b6a6 100644 --- a/app/src/main/res/layout/item_message_out.xml +++ b/app/src/main/res/layout/item_message_out.xml @@ -1,76 +1,74 @@ - + + + + android:animateLayoutChanges="true" + android:orientation="vertical"> - + - - - - - + android:layout_gravity="center_vertical|start" + android:padding="15dp" + android:textColor="?colorOnBackground" + tools:text="This is test" /> - + + - - + - + - - - - \ No newline at end of file + \ 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 index daf9fd72..dc0dd047 100644 --- a/app/src/main/res/layout/item_message_service.xml +++ b/app/src/main/res/layout/item_message_service.xml @@ -1,35 +1,31 @@ - + - + android:textColor="?colorOnBackground" + tools:text="Service" /> - - - - - - - \ No newline at end of file + + \ 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 index 75c5f768..fac928d2 100644 --- a/app/src/main/res/menu/activity_main_bottom.xml +++ b/app/src/main/res/menu/activity_main_bottom.xml @@ -2,7 +2,7 @@ diff --git a/app/src/main/res/navigation/login.xml b/app/src/main/res/navigation/login.xml deleted file mode 100644 index ebc0f0ba..00000000 --- a/app/src/main/res/navigation/login.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/main.xml b/app/src/main/res/navigation/main.xml deleted file mode 100644 index 91915b7b..00000000 --- a/app/src/main/res/navigation/main.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/messages.xml b/app/src/main/res/navigation/messages.xml deleted file mode 100644 index 911ca742..00000000 --- a/app/src/main/res/navigation/messages.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml deleted file mode 100644 index 0e9c1a79..00000000 --- a/app/src/main/res/navigation/nav_graph.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-night-v31/colors.xml b/app/src/main/res/values-night-v31/colors.xml deleted file mode 100644 index d5350c0e..00000000 --- a/app/src/main/res/values-night-v31/colors.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - @android:color/system_accent1_0 - @android:color/system_accent1_400 - @android:color/system_accent1_500 - @android:color/system_accent1_600 - - @android:color/system_accent2_200 - @android:color/system_accent2_700 - - @android:color/system_accent3_200 - @android:color/system_accent3_700 - - @android:color/system_neutral1_10 - @android:color/system_neutral1_50 - @android:color/system_neutral1_100 - @android:color/system_neutral1_800 - @android:color/system_neutral1_900 - - @android:color/system_neutral2_0 - @android:color/system_neutral2_10 - @android:color/system_neutral2_100 - @android:color/system_neutral2_500 - - \ No newline at end of file diff --git a/app/src/main/res/values-night/bools.xml b/app/src/main/res/values-night/bools.xml new file mode 100644 index 00000000..d1af27f2 --- /dev/null +++ b/app/src/main/res/values-night/bools.xml @@ -0,0 +1,7 @@ + + + + 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 index a7ba55f9..6fa63cdb 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -1,19 +1,48 @@ + @color/a1_200 + @color/a1_800 + @color/a1_700 + @color/a1_100 + @color/a1_600 + @color/a3_100 + + @color/a2_200 + @color/a2_800 + @color/a2_700 + @color/a2_100 + + @color/a3_200 + @color/a3_800 + @color/a3_700 + @color/a3_100 + + #f2b8b5 + #601410 + #8c1d18 + #f2b8b5 + + @color/n2_400 @color/n1_900 + @color/n1_100 + @color/n1_100_50 + @color/n2_800 + @color/a1_200 + @color/a1_900 - @color/colorBackground - @color/colorBackground - @color/n1_900 + @color/n1_900 + @color/n1_100 + @color/n2_700 + @color/n2_200 + @color/n1_100 + @color/n1_800 - @color/a2_100 - @color/a2_700 - @color/a2_700 + @color/n2_500 + @color/n2_10 - @color/n1_100 - @color/n1_100 - - @color/a1_0 + @color/n2_100 + @color/n2_400 + @color/a1_1000 \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml deleted file mode 100644 index cdff4e2e..00000000 --- a/app/src/main/res/values-night/themes.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-v31/colors.xml b/app/src/main/res/values-v31/colors.xml deleted file mode 100644 index d8bcc1e8..00000000 --- a/app/src/main/res/values-v31/colors.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - @android:color/system_accent1_0 - @android:color/system_accent1_200 - @android:color/system_accent1_400 - @android:color/system_accent1_500 - @android:color/system_accent1_600 - - @android:color/system_accent2_100 - @android:color/system_accent2_200 - @android:color/system_accent2_300 - @android:color/system_accent2_700 - @android:color/system_accent2_1000 - - @android:color/system_accent3_200 - @android:color/system_accent3_700 - - @android:color/system_neutral1_10 - @android:color/system_neutral1_50 - @android:color/system_neutral1_100 - @android:color/system_neutral1_800 - @android:color/system_neutral1_900 - - @android:color/system_neutral2_0 - @android:color/system_neutral2_10 - @android:color/system_neutral2_100 - @android:color/system_neutral2_500 - @android:color/system_neutral2_600 - - \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index c9703384..f98ec6d3 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -1,19 +1,6 @@ - - - - - - - - - - - - - @@ -44,4 +31,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/bools.xml b/app/src/main/res/values/bools.xml new file mode 100644 index 00000000..82d60b1a --- /dev/null +++ b/app/src/main/res/values/bools.xml @@ -0,0 +1,7 @@ + + + + true + true + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 5a658924..a5122111 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,61 +1,48 @@ + @color/a1_600 + @color/a1_0 + @color/a1_100 + @color/a1_900 + @color/a1_200 + @color/a3_200 - @color/n1_50 + @color/a2_600 + @color/a2_0 + @color/a2_100 + @color/a2_900 - @color/colorBackground - @color/colorBackground - @color/n1_900 + @color/a3_600 + @color/a3_0 + @color/a3_100 + @color/a3_900 - @color/a2_200 - @color/a2_700 - @color/a2_700 + #b3261e + @android:color/white + #f9dedc + #410e0b - @color/a3_200 - @color/a3_100 - @color/a3_700 + @color/n2_500 - @color/a1_600 - @color/a1_500 - @color/a1_0 + @color/n1_10 + @color/n1_900 + @color/n1_900_50 + @color/n1_100 + @color/a1_500 + @color/a1_0 - @color/n1_900 - @color/n1_900 - @color/n2_500 - @color/n2_600 + @color/n1_10 + @color/n1_900 + @color/n2_100 + @color/n2_700 + @color/n1_800 + @color/n1_50 + @color/n2_600 + @color/n2_10 - @color/a1_0 - - @color/n2_100 - @color/n1_10 - - #FFFFFF - #B1C6FA - #4184F5 - #3771DF - #2559BC - #000000 - - #DCE1F7 - #C0C6DA - #A4ABBF - #414757 - - #F8D6FC - #DEBAE5 - #583C61 - - #FBF9FC - #F1F1F1 - #E2E1E5 - #303033 - #1B1B1D - - #FFFFFF - #FDFBFE - #E0E2EB - #74767D - #5C5E65 + @color/n2_500 + @color/a2_300 + @color/a1_0 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index fe991af4..e8410884 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,4 +1,7 @@ 16dp 16dp + + + 84dp diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml index 0deb0bf0..a5069327 100644 --- a/app/src/main/res/values/ic_launcher_background.xml +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -1,5 +1,4 @@ - @color/a1_500 \ No newline at end of file diff --git a/app/src/main/res/values/monet_colors.xml b/app/src/main/res/values/monet_colors.xml new file mode 100644 index 00000000..5ce1bc65 --- /dev/null +++ b/app/src/main/res/values/monet_colors.xml @@ -0,0 +1,50 @@ + + + + #FFFFFF + #D8E1FC + #B1C6FA + #598DF7 + #3771DF + #2559BC + #194290 + #0F2D67 + #061A41 + #000000 + + #FEFEFE + #DCE1F7 + #C0C6DA + #A4ABBF + #585E6F + #414757 + #2A3040 + #151C2B + + #FFFFFF + #F8D6FC + #DEBAE5 + #715379 + #583C61 + #40254A + #2A0F33 + + #FBF9FC + #F1F1F1 + #E2E1E5 + #80E2E1E5 + #C7C6C9 + #919094 + #46464A + #303033 + #1B1B1D + #801B1B1D + + #FFFFFF + #FDFBFE + #E0E2EB + #74767D + #5C5E65 + #2F3037 + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8ea8a128..447ecc5f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -102,13 +102,14 @@ Current call Event Curator + Story + Widget %d bytes Community post User post Post - Story Log out Confirmation Signing out will delete all data related to this account from this device. Continue? @@ -148,4 +149,9 @@ Unknown Pin the message? Unpin the message? + + Messages + + Your story + Story from\n%s diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 517ae724..d606e5f0 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,44 +1,63 @@ -