+19
-10
@@ -7,9 +7,9 @@ plugins {
|
|||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
id("kotlin-kapt")
|
id("kotlin-kapt")
|
||||||
|
id("kotlin-parcelize")
|
||||||
id("androidx.navigation.safeargs.kotlin")
|
id("androidx.navigation.safeargs.kotlin")
|
||||||
id("dagger.hilt.android.plugin")
|
id("dagger.hilt.android.plugin")
|
||||||
id("kotlin-parcelize")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -38,6 +38,9 @@ android {
|
|||||||
getByName("release") {
|
getByName("release") {
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = false
|
||||||
|
|
||||||
|
buildConfigField("String", "vkLogin", login)
|
||||||
|
buildConfigField("String", "vkPassword", password)
|
||||||
|
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
@@ -68,7 +71,7 @@ android {
|
|||||||
kapt {
|
kapt {
|
||||||
correctErrorTypes = true
|
correctErrorTypes = true
|
||||||
|
|
||||||
//use this shit if you don't want to have hilt errors
|
//use this shit if you don't want have hilt errors
|
||||||
javacOptions {
|
javacOptions {
|
||||||
option("-Adagger.hilt.android.internal.disableAndroidSuperclassValidation=true")
|
option("-Adagger.hilt.android.internal.disableAndroidSuperclassValidation=true")
|
||||||
}
|
}
|
||||||
@@ -79,13 +82,19 @@ dependencies {
|
|||||||
|
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
|
||||||
|
|
||||||
|
implementation("com.github.massoudss:waveformSeekBar:3.1.0")
|
||||||
|
|
||||||
|
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
||||||
|
|
||||||
implementation("androidx.work:work-runtime-ktx:2.6.0")
|
implementation("androidx.work:work-runtime-ktx:2.6.0")
|
||||||
|
|
||||||
implementation("androidx.datastore:datastore-preferences:1.0.0")
|
implementation("androidx.datastore:datastore-preferences:1.0.0")
|
||||||
|
|
||||||
implementation("androidx.appcompat:appcompat:1.4.0-alpha03")
|
implementation("androidx.paging:paging-runtime-ktx:3.0.1")
|
||||||
implementation("com.google.android.material:material:1.5.0-alpha03")
|
|
||||||
implementation("androidx.core:core-ktx:1.7.0-beta01")
|
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.preference:preference-ktx:1.1.1")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
||||||
@@ -93,7 +102,7 @@ dependencies {
|
|||||||
implementation("androidx.fragment:fragment-ktx:1.3.6")
|
implementation("androidx.fragment:fragment-ktx:1.3.6")
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2-native-mt")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
|
||||||
|
|
||||||
implementation("androidx.room:room-ktx:2.3.0")
|
implementation("androidx.room:room-ktx:2.3.0")
|
||||||
implementation("androidx.room:room-runtime:2.3.0")
|
implementation("androidx.room:room-runtime:2.3.0")
|
||||||
@@ -113,16 +122,16 @@ dependencies {
|
|||||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||||
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
||||||
|
|
||||||
implementation("com.google.dagger:hilt-android:2.38.1")
|
implementation("com.google.dagger:hilt-android:2.39.1")
|
||||||
kapt("com.google.dagger:hilt-android-compiler:2.38.1")
|
kapt("com.google.dagger:hilt-android-compiler:2.39.1")
|
||||||
implementation("androidx.hilt:hilt-navigation-fragment:1.0.0")
|
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.3")
|
||||||
|
|
||||||
implementation("io.coil-kt:coil:1.3.2")
|
implementation("io.coil-kt:coil:1.4.0")
|
||||||
|
|
||||||
implementation("com.google.code.gson:gson:2.8.8")
|
implementation("com.google.code.gson:gson:2.8.8")
|
||||||
implementation("org.jsoup:jsoup:1.14.2")
|
implementation("org.jsoup:jsoup:1.14.3")
|
||||||
implementation("ch.acra:acra:4.11.1")
|
implementation("ch.acra:acra:4.11.1")
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
<application
|
<application
|
||||||
android:name=".common.AppGlobal"
|
android:name=".common.AppGlobal"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
|
android:extractNativeLibs="false"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
package com.meloda.fast.activity
|
package com.meloda.fast.activity
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import com.meloda.fast.R
|
import com.meloda.fast.R
|
||||||
import com.meloda.fast.base.BaseActivity
|
import com.meloda.fast.base.BaseActivity
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : BaseActivity(R.layout.activity_main)
|
class MainActivity : BaseActivity(R.layout.activity_main) {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
installSplashScreen()
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.meloda.fast.api
|
||||||
|
|
||||||
|
object ApiExtensions {
|
||||||
|
|
||||||
|
val Boolean.intString get() = (if (this) 1 else 0).toString()
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import com.meloda.fast.common.AppGlobal
|
|||||||
|
|
||||||
object UserConfig {
|
object UserConfig {
|
||||||
|
|
||||||
|
private const val FAST_TOKEN = "fast_token"
|
||||||
private const val TOKEN = "token"
|
private const val TOKEN = "token"
|
||||||
private const val USER_ID = "user_id"
|
private const val USER_ID = "user_id"
|
||||||
|
|
||||||
@@ -25,8 +26,16 @@ object UserConfig {
|
|||||||
AppGlobal.preferences.edit().putString(TOKEN, value).apply()
|
AppGlobal.preferences.edit().putString(TOKEN, value).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var fastToken: String = ""
|
||||||
|
get() = AppGlobal.preferences.getString(FAST_TOKEN, "") ?: ""
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
AppGlobal.preferences.edit().putString(FAST_TOKEN, value).apply()
|
||||||
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
accessToken = ""
|
accessToken = ""
|
||||||
|
fastToken = ""
|
||||||
userId = -1
|
userId = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.meloda.fast.api
|
package com.meloda.fast.api
|
||||||
|
|
||||||
|
import com.meloda.fast.api.model.attachments.*
|
||||||
|
|
||||||
object VKConstants {
|
object VKConstants {
|
||||||
|
|
||||||
const val GROUP_FIELDS = "description,members_count,counters,status,verified"
|
const val GROUP_FIELDS = "description,members_count,counters,status,verified"
|
||||||
@@ -7,6 +9,8 @@ object VKConstants {
|
|||||||
const val USER_FIELDS =
|
const val USER_FIELDS =
|
||||||
"photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex,online_info"
|
"photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex,online_info"
|
||||||
|
|
||||||
|
const val ALL_FIELDS = "$USER_FIELDS,$GROUP_FIELDS"
|
||||||
|
|
||||||
const val API_VERSION = "5.132"
|
const val API_VERSION = "5.132"
|
||||||
const val VK_APP_ID = "2274003"
|
const val VK_APP_ID = "2274003"
|
||||||
const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH"
|
const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH"
|
||||||
@@ -33,4 +37,15 @@ object VKConstants {
|
|||||||
const val PASSWORD = "password"
|
const val PASSWORD = "password"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val restrictedToEditAttachments = listOf(
|
||||||
|
VkCall::class.java,
|
||||||
|
VkCurator::class.java,
|
||||||
|
VkEvent::class.java,
|
||||||
|
VkGift::class.java,
|
||||||
|
VkGraffiti::class.java,
|
||||||
|
VkGroupCall::class.java,
|
||||||
|
VkStory::class.java,
|
||||||
|
VkVoiceMessage::class.java
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -8,11 +8,9 @@ open class VKException(
|
|||||||
var code: Int = -1,
|
var code: Int = -1,
|
||||||
var description: String = "",
|
var description: String = "",
|
||||||
var error: String
|
var error: String
|
||||||
) :
|
) : IOException(description) {
|
||||||
IOException(description) {
|
|
||||||
|
|
||||||
var captcha: Pair<String, String>? = null
|
// TODO: 10-Oct-21 remove this
|
||||||
var validationSid: String? = null
|
|
||||||
var json: JSONObject? = null
|
var json: JSONObject? = null
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.text.SpannableString
|
|||||||
import android.text.style.StyleSpan
|
import android.text.style.StyleSpan
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.meloda.fast.R
|
import com.meloda.fast.R
|
||||||
|
import com.meloda.fast.api.model.VkConversation
|
||||||
import com.meloda.fast.api.model.VkGroup
|
import com.meloda.fast.api.model.VkGroup
|
||||||
import com.meloda.fast.api.model.VkMessage
|
import com.meloda.fast.api.model.VkMessage
|
||||||
import com.meloda.fast.api.model.VkUser
|
import com.meloda.fast.api.model.VkUser
|
||||||
@@ -16,6 +17,88 @@ import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
|
|||||||
|
|
||||||
object VkUtils {
|
object VkUtils {
|
||||||
|
|
||||||
|
fun <T> attachmentToString(
|
||||||
|
attachmentClass: Class<T>,
|
||||||
|
id: Int,
|
||||||
|
ownerId: Int,
|
||||||
|
withAccessKey: Boolean,
|
||||||
|
accessKey: String?
|
||||||
|
): String {
|
||||||
|
val type = when (attachmentClass) {
|
||||||
|
VkAudio::class.java -> "audio"
|
||||||
|
VkFile::class.java -> "doc"
|
||||||
|
VkVideo::class.java -> "video"
|
||||||
|
VkPhoto::class.java -> "photo"
|
||||||
|
else -> throw IllegalArgumentException("unknown attachment class: $attachmentClass")
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = StringBuilder(type).append(ownerId).append('_').append(id)
|
||||||
|
if (withAccessKey && !accessKey.isNullOrBlank()) {
|
||||||
|
result.append('_')
|
||||||
|
result.append(accessKey)
|
||||||
|
}
|
||||||
|
return result.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun getMessageUser(message: VkMessage, profiles: Map<Int, VkUser>): VkUser? {
|
||||||
|
return (if (!message.isUser()) null
|
||||||
|
else profiles[message.fromId]).also { message.user.value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMessageGroup(message: VkMessage, groups: Map<Int, VkGroup>): VkGroup? {
|
||||||
|
return (if (!message.isGroup()) null
|
||||||
|
else groups[message.fromId]).also { message.group.value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMessageAvatar(
|
||||||
|
message: VkMessage,
|
||||||
|
messageUser: VkUser?,
|
||||||
|
messageGroup: VkGroup?
|
||||||
|
): String? {
|
||||||
|
return when {
|
||||||
|
message.isUser() -> messageUser?.photo200
|
||||||
|
message.isGroup() -> messageGroup?.photo200
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMessageTitle(
|
||||||
|
message: VkMessage,
|
||||||
|
messageUser: VkUser?,
|
||||||
|
messageGroup: VkGroup?
|
||||||
|
): String? {
|
||||||
|
return when {
|
||||||
|
message.isUser() -> messageUser?.fullName
|
||||||
|
message.isGroup() -> messageGroup?.name
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getConversationUser(conversation: VkConversation, profiles: Map<Int, VkUser>): VkUser? {
|
||||||
|
return (if (!conversation.isUser()) null
|
||||||
|
else profiles[conversation.id]).also { conversation.user.value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getConversationGroup(conversation: VkConversation, groups: Map<Int, VkGroup>): VkGroup? {
|
||||||
|
return (if (!conversation.isGroup()) null
|
||||||
|
else groups[conversation.id]).also { conversation.group.value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getConversationAvatar(
|
||||||
|
conversation: VkConversation,
|
||||||
|
conversationUser: VkUser?,
|
||||||
|
conversationGroup: VkGroup?
|
||||||
|
): String? {
|
||||||
|
return when {
|
||||||
|
conversation.ownerId == VKConstants.FAST_GROUP_ID -> null
|
||||||
|
conversation.isUser() -> conversationUser?.photo200
|
||||||
|
conversation.isGroup() -> conversationGroup?.photo200
|
||||||
|
conversation.isChat() -> conversation.photo200
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun prepareMessageText(text: String, forConversations: Boolean? = null): String {
|
fun prepareMessageText(text: String, forConversations: Boolean? = null): String {
|
||||||
return text.apply {
|
return text.apply {
|
||||||
if (forConversations == true) replace("\n", "")
|
if (forConversations == true) replace("\n", "")
|
||||||
@@ -42,6 +125,12 @@ object VkUtils {
|
|||||||
return forwards
|
return forwards
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun parseReplyMessage(baseReplyMessage: BaseVkMessage?): VkMessage? {
|
||||||
|
if (baseReplyMessage == null) return null
|
||||||
|
|
||||||
|
return baseReplyMessage.asVkMessage()
|
||||||
|
}
|
||||||
|
|
||||||
fun parseAttachments(baseAttachments: List<BaseVkAttachmentItem>?): List<VkAttachment>? {
|
fun parseAttachments(baseAttachments: List<BaseVkAttachmentItem>?): List<VkAttachment>? {
|
||||||
if (baseAttachments.isNullOrEmpty()) return null
|
if (baseAttachments.isNullOrEmpty()) return null
|
||||||
|
|
||||||
@@ -77,9 +166,7 @@ object VkUtils {
|
|||||||
}
|
}
|
||||||
BaseVkAttachmentItem.AttachmentType.VOICE -> {
|
BaseVkAttachmentItem.AttachmentType.VOICE -> {
|
||||||
val voiceMessage = baseAttachment.voiceMessage ?: continue
|
val voiceMessage = baseAttachment.voiceMessage ?: continue
|
||||||
attachments += VkVoiceMessage(
|
attachments += voiceMessage.asVkVoiceMessage()
|
||||||
link = voiceMessage.link_mp3
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
BaseVkAttachmentItem.AttachmentType.STICKER -> {
|
BaseVkAttachmentItem.AttachmentType.STICKER -> {
|
||||||
val sticker = baseAttachment.sticker ?: continue
|
val sticker = baseAttachment.sticker ?: continue
|
||||||
@@ -87,9 +174,7 @@ object VkUtils {
|
|||||||
}
|
}
|
||||||
BaseVkAttachmentItem.AttachmentType.GIFT -> {
|
BaseVkAttachmentItem.AttachmentType.GIFT -> {
|
||||||
val gift = baseAttachment.gift ?: continue
|
val gift = baseAttachment.gift ?: continue
|
||||||
attachments += VkGift(
|
attachments += gift.asVkGift()
|
||||||
link = gift.thumb_48
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
BaseVkAttachmentItem.AttachmentType.WALL -> {
|
BaseVkAttachmentItem.AttachmentType.WALL -> {
|
||||||
val wall = baseAttachment.wall ?: continue
|
val wall = baseAttachment.wall ?: continue
|
||||||
@@ -97,9 +182,7 @@ object VkUtils {
|
|||||||
}
|
}
|
||||||
BaseVkAttachmentItem.AttachmentType.GRAFFITI -> {
|
BaseVkAttachmentItem.AttachmentType.GRAFFITI -> {
|
||||||
val graffiti = baseAttachment.graffiti ?: continue
|
val graffiti = baseAttachment.graffiti ?: continue
|
||||||
attachments += VkGraffiti(
|
attachments += graffiti.asVkGraffiti()
|
||||||
link = graffiti.url
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
BaseVkAttachmentItem.AttachmentType.POLL -> {
|
BaseVkAttachmentItem.AttachmentType.POLL -> {
|
||||||
val poll = baseAttachment.poll ?: continue
|
val poll = baseAttachment.poll ?: continue
|
||||||
@@ -115,9 +198,7 @@ object VkUtils {
|
|||||||
}
|
}
|
||||||
BaseVkAttachmentItem.AttachmentType.CALL -> {
|
BaseVkAttachmentItem.AttachmentType.CALL -> {
|
||||||
val call = baseAttachment.call ?: continue
|
val call = baseAttachment.call ?: continue
|
||||||
attachments += VkCall(
|
attachments += call.asVkCall()
|
||||||
initiatorId = call.initiator_id
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS -> {
|
BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS -> {
|
||||||
val groupCall = baseAttachment.groupCall ?: continue
|
val groupCall = baseAttachment.groupCall ?: continue
|
||||||
|
|||||||
@@ -1,38 +1,50 @@
|
|||||||
package com.meloda.fast.api.model
|
package com.meloda.fast.api.model
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.room.Embedded
|
import androidx.room.Embedded
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Ignore
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Entity(tableName = "conversations")
|
@Entity(tableName = "conversations")
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class VkConversation(
|
data class VkConversation(
|
||||||
@PrimaryKey(autoGenerate = false)
|
@PrimaryKey(autoGenerate = false)
|
||||||
val id: Int,
|
var id: Int,
|
||||||
val ownerId: Int?,
|
var ownerId: Int?,
|
||||||
val title: String?,
|
var title: String?,
|
||||||
val photo200: String?,
|
var photo200: String?,
|
||||||
val type: String,
|
var type: String,
|
||||||
val callInProgress: Boolean,
|
var callInProgress: Boolean,
|
||||||
val isPhantom: Boolean,
|
var isPhantom: Boolean,
|
||||||
val lastConversationMessageId: Int,
|
var lastConversationMessageId: Int,
|
||||||
val inRead: Int,
|
var inRead: Int,
|
||||||
val outRead: Int,
|
var outRead: Int,
|
||||||
val isMarkedUnread: Boolean,
|
var isMarkedUnread: Boolean,
|
||||||
val lastMessageId: Int,
|
var lastMessageId: Int,
|
||||||
val unreadCount: Int?,
|
var unreadCount: Int?,
|
||||||
val membersCount: Int?,
|
var membersCount: Int?,
|
||||||
val isPinned: Boolean,
|
var isPinned: Boolean,
|
||||||
|
var canChangePin: Boolean,
|
||||||
|
|
||||||
@Embedded(prefix = "pinnedMessage_")
|
@Embedded(prefix = "pinnedMessage_")
|
||||||
var pinnedMessage: VkMessage? = null,
|
var pinnedMessage: VkMessage? = null,
|
||||||
|
|
||||||
@Embedded(prefix = "lastMessage_")
|
@Embedded(prefix = "lastMessage_")
|
||||||
var lastMessage: VkMessage? = null
|
var lastMessage: VkMessage? = null,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
|
val user = MutableLiveData<VkUser?>()
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
|
val group = MutableLiveData<VkGroup?>()
|
||||||
|
|
||||||
fun isChat() = type == "chat"
|
fun isChat() = type == "chat"
|
||||||
fun isUser() = type == "user"
|
fun isUser() = type == "user"
|
||||||
fun isGroup() = type == "group"
|
fun isGroup() = type == "group"
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
package com.meloda.fast.api.model
|
package com.meloda.fast.api.model
|
||||||
|
|
||||||
import android.os.Parcelable
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Ignore
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
import com.meloda.fast.api.UserConfig
|
||||||
|
import com.meloda.fast.api.VKConstants
|
||||||
import com.meloda.fast.api.model.attachments.VkAttachment
|
import com.meloda.fast.api.model.attachments.VkAttachment
|
||||||
|
import com.meloda.fast.base.adapter.SelectableItem
|
||||||
|
import com.meloda.fast.util.TimeUtils
|
||||||
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Entity(tableName = "messages")
|
@Entity(tableName = "messages")
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class VkMessage(
|
data class VkMessage(
|
||||||
@PrimaryKey(autoGenerate = false)
|
@PrimaryKey(autoGenerate = false)
|
||||||
val id: Int,
|
var id: Int,
|
||||||
val text: String? = null,
|
var text: String? = null,
|
||||||
val isOut: Boolean,
|
val isOut: Boolean,
|
||||||
val peerId: Int,
|
val peerId: Int,
|
||||||
val fromId: Int,
|
val fromId: Int,
|
||||||
@@ -23,10 +29,22 @@ data class VkMessage(
|
|||||||
val actionConversationMessageId: Int? = null,
|
val actionConversationMessageId: Int? = null,
|
||||||
val actionMessage: String? = null,
|
val actionMessage: String? = null,
|
||||||
val geoType: String? = null,
|
val geoType: String? = null,
|
||||||
val important: Boolean = false,
|
var important: Boolean = false,
|
||||||
|
|
||||||
var forwards: List<VkMessage>? = null,
|
var forwards: List<VkMessage>? = null,
|
||||||
var attachments: List<VkAttachment>? = null
|
var attachments: List<VkAttachment>? = null,
|
||||||
) : Parcelable {
|
|
||||||
|
// @Embedded(prefix = "replyMessage_")
|
||||||
|
var replyMessage: VkMessage? = null
|
||||||
|
) : SelectableItem() {
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
|
val user = MutableLiveData<VkUser?>()
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
|
val group = MutableLiveData<VkGroup?>()
|
||||||
|
|
||||||
fun isPeerChat() = peerId > 2_000_000_000
|
fun isPeerChat() = peerId > 2_000_000_000
|
||||||
|
|
||||||
@@ -43,40 +61,12 @@ data class VkMessage(
|
|||||||
return Action.parse(action)
|
return Action.parse(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun copyMessage(
|
fun canEdit() =
|
||||||
id: Int = this.id,
|
fromId == UserConfig.userId &&
|
||||||
text: String? = this.text,
|
(attachments == null || !VKConstants.restrictedToEditAttachments.contains(
|
||||||
isOut: Boolean = this.isOut,
|
attachments!![0].javaClass
|
||||||
peerId: Int = this.peerId,
|
)) &&
|
||||||
fromId: Int = this.fromId,
|
(System.currentTimeMillis() / 1000 - date.toLong() < TimeUtils.ONE_DAY_IN_SECONDS)
|
||||||
date: Int = this.date,
|
|
||||||
randomId: Int = this.randomId,
|
|
||||||
action: String? = this.action,
|
|
||||||
actionMemberId: Int? = this.actionMemberId,
|
|
||||||
actionText: String? = this.actionText,
|
|
||||||
actionConversationMessageId: Int? = this.actionConversationMessageId,
|
|
||||||
actionMessage: String? = this.actionMessage,
|
|
||||||
geoType: String? = this.geoType,
|
|
||||||
important: Boolean = this.important
|
|
||||||
) = VkMessage(
|
|
||||||
id = id,
|
|
||||||
text = text,
|
|
||||||
isOut = isOut,
|
|
||||||
peerId = peerId,
|
|
||||||
fromId = fromId,
|
|
||||||
date = date,
|
|
||||||
randomId = randomId,
|
|
||||||
action = action,
|
|
||||||
actionMemberId = actionMemberId,
|
|
||||||
actionText = actionText,
|
|
||||||
actionConversationMessageId = actionConversationMessageId,
|
|
||||||
actionMessage = actionMessage,
|
|
||||||
geoType = geoType,
|
|
||||||
important = important
|
|
||||||
).also {
|
|
||||||
it.attachments = attachments
|
|
||||||
it.forwards = forwards
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class Action(val value: String) {
|
enum class Action(val value: String) {
|
||||||
CHAT_CREATE("chat_create"),
|
CHAT_CREATE("chat_create"),
|
||||||
|
|||||||
@@ -4,4 +4,8 @@ import android.os.Parcelable
|
|||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
open class VkAttachment : Parcelable
|
open class VkAttachment : Parcelable {
|
||||||
|
|
||||||
|
open fun asString(withAccessKey: Boolean = true) = ""
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,17 +1,28 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
package com.meloda.fast.api.model.attachments
|
||||||
|
|
||||||
|
import com.meloda.fast.api.VkUtils
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class VkAudio(
|
data class VkAudio(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
|
val ownerId: Int,
|
||||||
val title: String,
|
val title: String,
|
||||||
val artist: String,
|
val artist: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
val duration: Int
|
val duration: Int,
|
||||||
|
val accessKey: String?
|
||||||
) : VkAttachment() {
|
) : VkAttachment() {
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
val className: String = this::class.java.name
|
val className: String = this::class.java.name
|
||||||
|
|
||||||
|
override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString(
|
||||||
|
attachmentClass = this::class.java,
|
||||||
|
id = id,
|
||||||
|
ownerId = ownerId,
|
||||||
|
withAccessKey = withAccessKey,
|
||||||
|
accessKey = accessKey
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,12 @@ import kotlinx.parcelize.Parcelize
|
|||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class VkCall(
|
data class VkCall(
|
||||||
val initiatorId: Int
|
val initiatorId: Int,
|
||||||
|
val receiverId: Int,
|
||||||
|
val state: String,
|
||||||
|
val time: Int,
|
||||||
|
val duration: Int,
|
||||||
|
val isVideo: Boolean
|
||||||
) : VkAttachment() {
|
) : VkAttachment() {
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
package com.meloda.fast.api.model.attachments
|
||||||
|
|
||||||
|
import com.meloda.fast.api.VkUtils
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class VkFile(
|
data class VkFile(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
|
val ownerId: Int,
|
||||||
val title: String,
|
val title: String,
|
||||||
val ext: String,
|
val ext: String,
|
||||||
val size: Int,
|
val size: Int,
|
||||||
val url: String
|
val url: String,
|
||||||
|
val accessKey: String?
|
||||||
) : VkAttachment() {
|
) : VkAttachment() {
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
val className: String = this::class.java.name
|
val className: String = this::class.java.name
|
||||||
|
|
||||||
|
override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString(
|
||||||
|
attachmentClass = this::class.java,
|
||||||
|
id = id,
|
||||||
|
ownerId = ownerId,
|
||||||
|
withAccessKey = withAccessKey,
|
||||||
|
accessKey = accessKey
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,10 @@ import kotlinx.parcelize.Parcelize
|
|||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class VkGift(
|
data class VkGift(
|
||||||
val link: String
|
val id: Int,
|
||||||
|
val thumb256: String?,
|
||||||
|
val thumb96: String?,
|
||||||
|
val thumb48: String
|
||||||
) : VkAttachment() {
|
) : VkAttachment() {
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ import kotlinx.parcelize.Parcelize
|
|||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class VkGraffiti(
|
data class VkGraffiti(
|
||||||
val link: String
|
val id: Int,
|
||||||
|
val ownerId: Int,
|
||||||
|
val url: String,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
val accessKey: String
|
||||||
) : VkAttachment() {
|
) : VkAttachment() {
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ data class VkLink(
|
|||||||
val title: String?,
|
val title: String?,
|
||||||
val caption: String?,
|
val caption: String?,
|
||||||
val photo: VkPhoto?,
|
val photo: VkPhoto?,
|
||||||
val target: String,
|
val target: String?,
|
||||||
val isFavorite: Boolean
|
val isFavorite: Boolean
|
||||||
) : VkAttachment() {
|
) : VkAttachment() {
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
package com.meloda.fast.api.model.attachments
|
||||||
|
|
||||||
|
import androidx.room.Ignore
|
||||||
|
import com.meloda.fast.api.VkUtils
|
||||||
import com.meloda.fast.api.model.base.attachments.BaseVkPhoto
|
import com.meloda.fast.api.model.base.attachments.BaseVkPhoto
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class VkPhoto(
|
data class VkPhoto(
|
||||||
@@ -13,18 +16,71 @@ data class VkPhoto(
|
|||||||
val hasTags: Boolean,
|
val hasTags: Boolean,
|
||||||
val accessKey: String?,
|
val accessKey: String?,
|
||||||
val sizes: List<BaseVkPhoto.Size>,
|
val sizes: List<BaseVkPhoto.Size>,
|
||||||
val text: String,
|
val text: String?,
|
||||||
val userId: Int?
|
val userId: Int?
|
||||||
) : VkAttachment() {
|
) : VkAttachment() {
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
|
private val sizesChars = Stack<Char>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
sizesChars.push('s')
|
||||||
|
sizesChars.push('m')
|
||||||
|
sizesChars.push('x')
|
||||||
|
sizesChars.push('o')
|
||||||
|
sizesChars.push('p')
|
||||||
|
sizesChars.push('q')
|
||||||
|
sizesChars.push('r')
|
||||||
|
sizesChars.push('y')
|
||||||
|
sizesChars.push('z')
|
||||||
|
sizesChars.push('w')
|
||||||
|
}
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
val className: String = this::class.java.name
|
val className: String = this::class.java.name
|
||||||
|
|
||||||
fun sizeOfType(type: Char): BaseVkPhoto.Size? {
|
override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString(
|
||||||
|
attachmentClass = this::class.java,
|
||||||
|
id = id,
|
||||||
|
ownerId = ownerId,
|
||||||
|
withAccessKey = withAccessKey,
|
||||||
|
accessKey = accessKey
|
||||||
|
)
|
||||||
|
|
||||||
|
fun getMaxSize(): BaseVkPhoto.Size? {
|
||||||
|
return getSizeOrSmaller(sizesChars.peek())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSizeOrNull(type: Char): BaseVkPhoto.Size? {
|
||||||
for (size in sizes) {
|
for (size in sizes) {
|
||||||
if (size.type == type.toString())
|
if (size.type == type.toString()) return size
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSizeOrSmaller(type: Char): BaseVkPhoto.Size? {
|
||||||
|
val photoStack = sizesChars.clone() as Stack<Char>
|
||||||
|
|
||||||
|
val sizeIndex = photoStack.search(type)
|
||||||
|
|
||||||
|
if (sizeIndex == -1) return null
|
||||||
|
|
||||||
|
for (i in 0 until sizeIndex) {
|
||||||
|
photoStack.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in 0 until photoStack.size) {
|
||||||
|
val size = getSizeOrNull(photoStack.peek())
|
||||||
|
|
||||||
|
if (size == null) {
|
||||||
|
photoStack.pop()
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
package com.meloda.fast.api.model.attachments
|
||||||
|
|
||||||
|
import com.meloda.fast.api.VkUtils
|
||||||
import com.meloda.fast.api.model.base.attachments.BaseVkVideo
|
import com.meloda.fast.api.model.base.attachments.BaseVkVideo
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
@@ -7,8 +8,10 @@ import kotlinx.parcelize.Parcelize
|
|||||||
@Parcelize
|
@Parcelize
|
||||||
data class VkVideo(
|
data class VkVideo(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
|
val ownerId: Int,
|
||||||
val images: List<BaseVkVideo.Image>,
|
val images: List<BaseVkVideo.Image>,
|
||||||
val firstFrames: List<BaseVkVideo.FirstFrame>?
|
val firstFrames: List<BaseVkVideo.FirstFrame>?,
|
||||||
|
val accessKey: String?
|
||||||
) : VkAttachment() {
|
) : VkAttachment() {
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
@@ -18,4 +21,12 @@ data class VkVideo(
|
|||||||
return images.find { it.width == width }
|
return images.find { it.width == width }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString(
|
||||||
|
attachmentClass = this::class.java,
|
||||||
|
id = id,
|
||||||
|
ownerId = ownerId,
|
||||||
|
withAccessKey = withAccessKey,
|
||||||
|
accessKey = accessKey
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -5,9 +5,18 @@ import kotlinx.parcelize.Parcelize
|
|||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class VkVoiceMessage(
|
data class VkVoiceMessage(
|
||||||
val link: String
|
val id: Int,
|
||||||
|
val ownerId: Int,
|
||||||
|
val duration: Int,
|
||||||
|
val waveform: List<Int>,
|
||||||
|
val linkOgg: String,
|
||||||
|
val linkMp3: String,
|
||||||
|
val accessKey: String,
|
||||||
|
val transcriptState: String,
|
||||||
|
val transcript: String
|
||||||
) : VkAttachment() {
|
) : VkAttachment() {
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
val className: String = this::class.java.name
|
val className: String = this::class.java.name
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,8 @@ data class BaseVkConversation(
|
|||||||
unreadCount = unread_count,
|
unreadCount = unread_count,
|
||||||
membersCount = chat_settings?.members_count,
|
membersCount = chat_settings?.members_count,
|
||||||
ownerId = chat_settings?.owner_id,
|
ownerId = chat_settings?.owner_id,
|
||||||
isPinned = sort_id.major_id > 0
|
isPinned = sort_id.major_id > 0,
|
||||||
|
canChangePin = chat_settings?.acl?.can_change_pin == true
|
||||||
).apply {
|
).apply {
|
||||||
this.lastMessage = lastMessage
|
this.lastMessage = lastMessage
|
||||||
this.pinnedMessage = chat_settings?.pinned_message?.asVkMessage()
|
this.pinnedMessage = chat_settings?.pinned_message?.asVkMessage()
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ data class BaseVkMessage(
|
|||||||
val payload: String,
|
val payload: String,
|
||||||
val geo: Geo?,
|
val geo: Geo?,
|
||||||
val action: Action?,
|
val action: Action?,
|
||||||
val ttl: Int
|
val ttl: Int,
|
||||||
|
val reply_message: BaseVkMessage?
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
fun asVkMessage() = VkMessage(
|
fun asVkMessage() = VkMessage(
|
||||||
@@ -44,6 +45,7 @@ data class BaseVkMessage(
|
|||||||
).also {
|
).also {
|
||||||
it.attachments = VkUtils.parseAttachments(attachments)
|
it.attachments = VkUtils.parseAttachments(attachments)
|
||||||
it.forwards = VkUtils.parseForwards(fwd_messages)
|
it.forwards = VkUtils.parseForwards(fwd_messages)
|
||||||
|
it.replyMessage = VkUtils.parseReplyMessage(reply_message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ data class BaseVkAudio(
|
|||||||
val url: String,
|
val url: String,
|
||||||
val date: Int,
|
val date: Int,
|
||||||
val owner_id: Int,
|
val owner_id: Int,
|
||||||
val access_key: String,
|
val access_key: String?,
|
||||||
val is_explicit: Boolean,
|
val is_explicit: Boolean,
|
||||||
val is_focus_track: Boolean,
|
val is_focus_track: Boolean,
|
||||||
val is_licensed: Boolean,
|
val is_licensed: Boolean,
|
||||||
@@ -27,10 +27,12 @@ data class BaseVkAudio(
|
|||||||
|
|
||||||
fun asVkAudio() = VkAudio(
|
fun asVkAudio() = VkAudio(
|
||||||
id = id,
|
id = id,
|
||||||
|
ownerId = owner_id,
|
||||||
title = title,
|
title = title,
|
||||||
artist = artist,
|
artist = artist,
|
||||||
url = url,
|
url = url,
|
||||||
duration = duration
|
duration = duration,
|
||||||
|
accessKey = access_key
|
||||||
)
|
)
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
package com.meloda.fast.api.model.base.attachments
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.meloda.fast.api.model.attachments.VkCall
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -11,4 +12,15 @@ data class BaseVkCall(
|
|||||||
val time: Int,
|
val time: Int,
|
||||||
val duration: Int,
|
val duration: Int,
|
||||||
val video: Boolean
|
val video: Boolean
|
||||||
) : Parcelable
|
) : Parcelable {
|
||||||
|
|
||||||
|
fun asVkCall() = VkCall(
|
||||||
|
initiatorId = initiator_id,
|
||||||
|
receiverId = receiver_id,
|
||||||
|
state = state,
|
||||||
|
time = time,
|
||||||
|
duration = duration,
|
||||||
|
isVideo = video
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -16,16 +16,18 @@ data class BaseVkFile(
|
|||||||
val url: String,
|
val url: String,
|
||||||
val preview: Preview?,
|
val preview: Preview?,
|
||||||
val ic_licensed: Int,
|
val ic_licensed: Int,
|
||||||
val access_key: String,
|
val access_key: String?,
|
||||||
val web_preview_url: String?
|
val web_preview_url: String?
|
||||||
) : BaseVkAttachment() {
|
) : BaseVkAttachment() {
|
||||||
|
|
||||||
fun asVkFile() = VkFile(
|
fun asVkFile() = VkFile(
|
||||||
id = id,
|
id = id,
|
||||||
|
ownerId = owner_id,
|
||||||
title = title,
|
title = title,
|
||||||
ext = ext,
|
ext = ext,
|
||||||
url = url,
|
url = url,
|
||||||
size = size
|
size = size,
|
||||||
|
accessKey = access_key
|
||||||
)
|
)
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
package com.meloda.fast.api.model.base.attachments
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.meloda.fast.api.model.attachments.VkGift
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -9,4 +10,13 @@ data class BaseVkGift(
|
|||||||
val thumb_256: String?,
|
val thumb_256: String?,
|
||||||
val thumb_96: String?,
|
val thumb_96: String?,
|
||||||
val thumb_48: String
|
val thumb_48: String
|
||||||
) : Parcelable
|
) : Parcelable {
|
||||||
|
|
||||||
|
fun asVkGift() = VkGift(
|
||||||
|
id = id,
|
||||||
|
thumb256 = thumb_256,
|
||||||
|
thumb96 = thumb_96,
|
||||||
|
thumb48 = thumb_48
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
package com.meloda.fast.api.model.base.attachments
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.meloda.fast.api.model.attachments.VkGraffiti
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -11,4 +12,15 @@ data class BaseVkGraffiti(
|
|||||||
val width: Int,
|
val width: Int,
|
||||||
val height: Int,
|
val height: Int,
|
||||||
val access_key: String
|
val access_key: String
|
||||||
) : Parcelable
|
) : Parcelable {
|
||||||
|
|
||||||
|
fun asVkGraffiti() = VkGraffiti(
|
||||||
|
id = id,
|
||||||
|
ownerId = owner_id,
|
||||||
|
url = url,
|
||||||
|
width = width,
|
||||||
|
height = height,
|
||||||
|
accessKey = access_key
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ data class BaseVkLink(
|
|||||||
val title: String?,
|
val title: String?,
|
||||||
val caption: String?,
|
val caption: String?,
|
||||||
val photo: BaseVkPhoto?,
|
val photo: BaseVkPhoto?,
|
||||||
val target: String,
|
val target: String?,
|
||||||
val is_favorite: Boolean
|
val is_favorite: Boolean
|
||||||
) : BaseVkAttachment() {
|
) : BaseVkAttachment() {
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ data class BaseVkPhoto(
|
|||||||
val has_tags: Boolean,
|
val has_tags: Boolean,
|
||||||
val access_key: String?,
|
val access_key: String?,
|
||||||
val sizes: List<Size>,
|
val sizes: List<Size>,
|
||||||
val text: String,
|
val text: String?,
|
||||||
val user_id: Int?,
|
val user_id: Int?,
|
||||||
val lat: Double?,
|
val lat: Double?,
|
||||||
val long: Double?,
|
val long: Double?,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ data class BaseVkVideo(
|
|||||||
val can_add_to_faves: Int,
|
val can_add_to_faves: Int,
|
||||||
val can_add: Int,
|
val can_add: Int,
|
||||||
val can_attach_link: Int,
|
val can_attach_link: Int,
|
||||||
val access_key: String,
|
val access_key: String?,
|
||||||
val owner_id: Int,
|
val owner_id: Int,
|
||||||
val ov_id: String,
|
val ov_id: String,
|
||||||
val is_favorite: Boolean,
|
val is_favorite: Boolean,
|
||||||
@@ -40,8 +40,10 @@ data class BaseVkVideo(
|
|||||||
|
|
||||||
fun asVkVideo() = VkVideo(
|
fun asVkVideo() = VkVideo(
|
||||||
id = id,
|
id = id,
|
||||||
|
ownerId = owner_id,
|
||||||
images = image,
|
images = image,
|
||||||
firstFrames = first_frame
|
firstFrames = first_frame,
|
||||||
|
accessKey = access_key
|
||||||
)
|
)
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|||||||
+16
-1
@@ -1,6 +1,7 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
package com.meloda.fast.api.model.base.attachments
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.meloda.fast.api.model.attachments.VkVoiceMessage
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -14,4 +15,18 @@ data class BaseVkVoiceMessage(
|
|||||||
val access_key: String,
|
val access_key: String,
|
||||||
val transcript_state: String,
|
val transcript_state: String,
|
||||||
val transcript: String
|
val transcript: String
|
||||||
) : Parcelable
|
) : Parcelable {
|
||||||
|
|
||||||
|
fun asVkVoiceMessage() = VkVoiceMessage(
|
||||||
|
id = id,
|
||||||
|
ownerId = owner_id,
|
||||||
|
duration = duration,
|
||||||
|
waveform = waveform,
|
||||||
|
linkOgg = link_ogg,
|
||||||
|
linkMp3 = link_mp3,
|
||||||
|
accessKey = access_key,
|
||||||
|
transcriptState = transcript_state,
|
||||||
|
transcript = transcript
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.request
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class MessagesGetHistoryRequest(
|
|
||||||
val count: Int? = null,
|
|
||||||
val offset: Int? = null,
|
|
||||||
val peerId: Int,
|
|
||||||
val extended: Boolean? = null,
|
|
||||||
val startMessageId: Int? = null,
|
|
||||||
val rev: Boolean? = null,
|
|
||||||
val fields: String? = null,
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
val map
|
|
||||||
get() = mutableMapOf(
|
|
||||||
"peer_id" to peerId.toString()
|
|
||||||
).apply {
|
|
||||||
count?.let { this["count"] = it.toString() }
|
|
||||||
offset?.let { this["offset"] = it.toString() }
|
|
||||||
extended?.let { this["extended"] = (if (it) 1 else 0).toString() }
|
|
||||||
startMessageId?.let { this["start_message_id"] = it.toString() }
|
|
||||||
rev?.let { this["rev"] = (if (it) 1 else 0).toString() }
|
|
||||||
fields?.let { this["fields"] = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class MessagesSendRequest(
|
|
||||||
val peerId: Int,
|
|
||||||
val randomId: Int = 0,
|
|
||||||
val message: String? = null,
|
|
||||||
val lat: Int? = null,
|
|
||||||
val lon: Int? = null,
|
|
||||||
val replyTo: Int? = null,
|
|
||||||
val stickerId: Int? = null,
|
|
||||||
val disableMentions: Boolean? = null,
|
|
||||||
val dontParseLinks: Boolean? = null
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
val map
|
|
||||||
get() = mutableMapOf(
|
|
||||||
"peer_id" to peerId.toString(),
|
|
||||||
"random_id" to randomId.toString()
|
|
||||||
).apply {
|
|
||||||
message?.let { this["message"] = it }
|
|
||||||
lat?.let { this["lat"] = it.toString() }
|
|
||||||
lon?.let { this["lon"] = it.toString() }
|
|
||||||
replyTo?.let { this["reply_to"] = it.toString() }
|
|
||||||
stickerId?.let { this["sticker_id"] = it.toString() }
|
|
||||||
disableMentions?.let { this["disable_mentions"] = (if (it) 1 else 0).toString() }
|
|
||||||
dontParseLinks?.let { this["dont_parse_links"] = (if (it) 1 else 0).toString() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class MessagesMarkAsImportantRequest(
|
|
||||||
val messagesIds: List<Int>,
|
|
||||||
val important: Boolean
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
val map
|
|
||||||
get() = mutableMapOf(
|
|
||||||
"message_ids" to messagesIds.joinToString { it.toString() },
|
|
||||||
"important" to (if (important) 1 else 0).toString()
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class MessagesGetLongPollServerRequest(
|
|
||||||
val needPts: Boolean,
|
|
||||||
val version: Int
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
val map
|
|
||||||
get() = mutableMapOf(
|
|
||||||
"need_pts" to (if (needPts) 1 else 0).toString(),
|
|
||||||
"version" to version.toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
package com.meloda.fast.api.model.response
|
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ class AuthInterceptor : Interceptor {
|
|||||||
builder.addQueryParameter("access_token", URLEncoder.encode(it, "utf-8"))
|
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())
|
return chain.proceed(chain.request().newBuilder().apply { url(builder.build()) }.build())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.meloda.fast.api.network
|
package com.meloda.fast.api.network
|
||||||
|
|
||||||
import com.meloda.fast.api.VKException
|
import com.meloda.fast.api.VKException
|
||||||
|
import com.meloda.fast.api.base.ApiError
|
||||||
import com.meloda.fast.api.base.ApiResponse
|
import com.meloda.fast.api.base.ApiResponse
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okio.IOException
|
import okio.IOException
|
||||||
@@ -93,7 +94,6 @@ internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy)
|
|||||||
|
|
||||||
if (result is Answer.Error && isVkException) if (checkErrors(call, result)) return
|
if (result is Answer.Error && isVkException) if (checkErrors(call, result)) return
|
||||||
|
|
||||||
|
|
||||||
callback.onResponse(proxy, Response.success(result))
|
callback.onResponse(proxy, Response.success(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +105,11 @@ internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy)
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun checkErrors(call: Call<T>, result: Answer.Error): Boolean {
|
private fun checkErrors(call: Call<T>, result: Answer.Error): Boolean {
|
||||||
|
if (result.throwable is ApiError) {
|
||||||
|
onFailure(call, result.throwable)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
val json = JSONObject(result.throwable.message ?: "{}")
|
val json = JSONObject(result.throwable.message ?: "{}")
|
||||||
|
|
||||||
return if (json.has("error")) {
|
return if (json.has("error")) {
|
||||||
|
|||||||
+9
-1
@@ -1,6 +1,6 @@
|
|||||||
package com.meloda.fast.api.network
|
package com.meloda.fast.api.network
|
||||||
|
|
||||||
object VKUrls {
|
object VkUrls {
|
||||||
|
|
||||||
const val OAUTH = "https://oauth.vk.com"
|
const val OAUTH = "https://oauth.vk.com"
|
||||||
const val API = "https://api.vk.com/method"
|
const val API = "https://api.vk.com/method"
|
||||||
@@ -12,6 +12,10 @@ object VKUrls {
|
|||||||
|
|
||||||
object Conversations {
|
object Conversations {
|
||||||
const val Get = "$API/messages.getConversations"
|
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 {
|
object Users {
|
||||||
@@ -24,6 +28,10 @@ object VKUrls {
|
|||||||
const val MarkAsImportant = "$API/messages.markAsImportant"
|
const val MarkAsImportant = "$API/messages.markAsImportant"
|
||||||
const val GetLongPollServer = "$API/messages.getLongPollServer"
|
const val GetLongPollServer = "$API/messages.getLongPollServer"
|
||||||
const val GetLongPollHistory = "$API/messages.getLongPollHistory"
|
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
+1
-3
@@ -1,7 +1,5 @@
|
|||||||
package com.meloda.fast.api.network.datasource
|
package com.meloda.fast.api.network.auth
|
||||||
|
|
||||||
import com.meloda.fast.api.network.repo.AuthRepo
|
|
||||||
import com.meloda.fast.api.model.request.RequestAuthDirect
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AuthDataSource @Inject constructor(
|
class AuthDataSource @Inject constructor(
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.meloda.fast.api.network.auth
|
||||||
|
|
||||||
|
import com.meloda.fast.api.network.Answer
|
||||||
|
import com.meloda.fast.api.network.VkUrls
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Query
|
||||||
|
import retrofit2.http.QueryMap
|
||||||
|
|
||||||
|
interface AuthRepo {
|
||||||
|
|
||||||
|
@GET(VkUrls.Auth.DirectAuth)
|
||||||
|
suspend fun auth(@QueryMap param: Map<String, String?>): Answer<ResponseAuthDirect>
|
||||||
|
|
||||||
|
@GET(VkUrls.Auth.SendSms)
|
||||||
|
suspend fun sendSms(@Query("sid") validationSid: String): Answer<ResponseSendSms>
|
||||||
|
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.meloda.fast.api.model.request
|
package com.meloda.fast.api.network.auth
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.meloda.fast.api.model.response
|
package com.meloda.fast.api.network.auth
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package com.meloda.fast.api.network.conversations
|
||||||
|
|
||||||
|
import com.meloda.fast.api.model.VkConversation
|
||||||
|
import com.meloda.fast.database.dao.ConversationsDao
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ConversationsDataSource @Inject constructor(
|
||||||
|
private val repo: ConversationsRepo,
|
||||||
|
private val dao: ConversationsDao
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun get(params: ConversationsGetRequest) = repo.get(params.map)
|
||||||
|
|
||||||
|
suspend fun delete(params: ConversationsDeleteRequest) = repo.delete(params.map)
|
||||||
|
|
||||||
|
suspend fun pin(params: ConversationsPinRequest) = repo.pin(params.map)
|
||||||
|
|
||||||
|
suspend fun unpin(params: ConversationsUnpinRequest) = repo.unpin(params.map)
|
||||||
|
|
||||||
|
suspend fun store(conversations: List<VkConversation>) = dao.insert(conversations)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.meloda.fast.api.network.conversations
|
||||||
|
|
||||||
|
import com.meloda.fast.api.base.ApiResponse
|
||||||
|
import com.meloda.fast.api.network.Answer
|
||||||
|
import com.meloda.fast.api.network.VkUrls
|
||||||
|
import retrofit2.http.FieldMap
|
||||||
|
import retrofit2.http.FormUrlEncoded
|
||||||
|
import retrofit2.http.POST
|
||||||
|
|
||||||
|
interface ConversationsRepo {
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(VkUrls.Conversations.Get)
|
||||||
|
suspend fun get(@FieldMap params: Map<String, String>): Answer<ApiResponse<ConversationsGetResponse>>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(VkUrls.Conversations.Delete)
|
||||||
|
suspend fun delete(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(VkUrls.Conversations.Pin)
|
||||||
|
suspend fun pin(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(VkUrls.Conversations.Unpin)
|
||||||
|
suspend fun unpin(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(VkUrls.Conversations.ReorderPinned)
|
||||||
|
suspend fun reorderPinned(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
|
||||||
|
|
||||||
|
}
|
||||||
+16
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.meloda.fast.api.model.request
|
package com.meloda.fast.api.network.conversations
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
@@ -24,3 +24,18 @@ data class ConversationsGetRequest(
|
|||||||
startMessageId?.let { this["start_message_id"] = it.toString() }
|
startMessageId?.let { this["start_message_id"] = it.toString() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ConversationsDeleteRequest(val peerId: Int) : Parcelable {
|
||||||
|
val map get() = mapOf("peer_id" to peerId.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ConversationsPinRequest(val peerId: Int) : Parcelable {
|
||||||
|
val map get() = mapOf("peer_id" to peerId.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ConversationsUnpinRequest(val peerId: Int) : Parcelable {
|
||||||
|
val map get() = mapOf("peer_id" to peerId.toString())
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.meloda.fast.api.model.response
|
package com.meloda.fast.api.network.conversations
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.meloda.fast.api.network.datasource
|
|
||||||
|
|
||||||
import com.meloda.fast.api.model.VkConversation
|
|
||||||
import com.meloda.fast.api.network.repo.ConversationsRepo
|
|
||||||
import com.meloda.fast.api.model.request.ConversationsGetRequest
|
|
||||||
import com.meloda.fast.database.dao.ConversationsDao
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class ConversationsDataSource @Inject constructor(
|
|
||||||
private val repo: ConversationsRepo,
|
|
||||||
private val dao: ConversationsDao
|
|
||||||
) {
|
|
||||||
|
|
||||||
suspend fun getAllChats(params: ConversationsGetRequest) = repo.getAllChats(params.map)
|
|
||||||
|
|
||||||
suspend fun storeConversations(conversations: List<VkConversation>) = dao.insert(conversations)
|
|
||||||
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.meloda.fast.api.network.repo
|
package com.meloda.fast.api.network.longpoll
|
||||||
|
|
||||||
import com.meloda.fast.api.base.ApiResponse
|
import com.meloda.fast.api.base.ApiResponse
|
||||||
import com.meloda.fast.api.network.Answer
|
import com.meloda.fast.api.network.Answer
|
||||||
+15
-8
@@ -1,11 +1,6 @@
|
|||||||
package com.meloda.fast.api.network.datasource
|
package com.meloda.fast.api.network.messages
|
||||||
|
|
||||||
import com.meloda.fast.api.model.VkMessage
|
import com.meloda.fast.api.model.VkMessage
|
||||||
import com.meloda.fast.api.model.request.MessagesGetHistoryRequest
|
|
||||||
import com.meloda.fast.api.model.request.MessagesGetLongPollServerRequest
|
|
||||||
import com.meloda.fast.api.model.request.MessagesMarkAsImportantRequest
|
|
||||||
import com.meloda.fast.api.model.request.MessagesSendRequest
|
|
||||||
import com.meloda.fast.api.network.repo.MessagesRepo
|
|
||||||
import com.meloda.fast.database.dao.MessagesDao
|
import com.meloda.fast.database.dao.MessagesDao
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -26,8 +21,20 @@ class MessagesDataSource @Inject constructor(
|
|||||||
suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) =
|
suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) =
|
||||||
repo.getLongPollServer(params.map)
|
repo.getLongPollServer(params.map)
|
||||||
|
|
||||||
suspend fun storeMessages(messages: List<VkMessage>) = dao.insert(messages)
|
suspend fun pin(params: MessagesPinMessageRequest) =
|
||||||
|
repo.pin(params.map)
|
||||||
|
|
||||||
suspend fun getCachedMessages(peerId: Int) = dao.getByPeerId(peerId)
|
suspend fun unpin(params: MessagesUnPinMessageRequest) =
|
||||||
|
repo.unpin(params.map)
|
||||||
|
|
||||||
|
suspend fun delete(params: MessagesDeleteRequest) =
|
||||||
|
repo.delete(params.map)
|
||||||
|
|
||||||
|
suspend fun edit(params: MessagesEditRequest) =
|
||||||
|
repo.edit(params.map)
|
||||||
|
|
||||||
|
suspend fun store(messages: List<VkMessage>) = dao.insert(messages)
|
||||||
|
|
||||||
|
suspend fun getCached(peerId: Int) = dao.getByPeerId(peerId)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.meloda.fast.api.network.messages
|
||||||
|
|
||||||
|
import com.meloda.fast.api.base.ApiResponse
|
||||||
|
import com.meloda.fast.api.model.base.BaseVkLongPoll
|
||||||
|
import com.meloda.fast.api.model.base.BaseVkMessage
|
||||||
|
import com.meloda.fast.api.network.Answer
|
||||||
|
import com.meloda.fast.api.network.VkUrls
|
||||||
|
import retrofit2.http.FieldMap
|
||||||
|
import retrofit2.http.FormUrlEncoded
|
||||||
|
import retrofit2.http.POST
|
||||||
|
|
||||||
|
interface MessagesRepo {
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(VkUrls.Messages.GetHistory)
|
||||||
|
suspend fun getHistory(@FieldMap params: Map<String, String>): Answer<ApiResponse<MessagesGetHistoryResponse>>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(VkUrls.Messages.Send)
|
||||||
|
suspend fun send(@FieldMap params: Map<String, String>): Answer<ApiResponse<Int>>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(VkUrls.Messages.MarkAsImportant)
|
||||||
|
suspend fun markAsImportant(@FieldMap params: Map<String, String>): Answer<ApiResponse<List<Int>>>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(VkUrls.Messages.GetLongPollServer)
|
||||||
|
suspend fun getLongPollServer(@FieldMap params: Map<String, String>): Answer<ApiResponse<BaseVkLongPoll>>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(VkUrls.Messages.Pin)
|
||||||
|
suspend fun pin(@FieldMap params: Map<String, String>): Answer<ApiResponse<BaseVkMessage>>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(VkUrls.Messages.Unpin)
|
||||||
|
suspend fun unpin(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(VkUrls.Messages.Delete)
|
||||||
|
suspend fun delete(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(VkUrls.Messages.Edit)
|
||||||
|
suspend fun edit(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package com.meloda.fast.api.network.messages
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.meloda.fast.api.ApiExtensions.intString
|
||||||
|
import com.meloda.fast.api.model.attachments.VkAttachment
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class MessagesGetHistoryRequest(
|
||||||
|
val count: Int? = null,
|
||||||
|
val offset: Int? = null,
|
||||||
|
val peerId: Int,
|
||||||
|
val extended: Boolean? = null,
|
||||||
|
val startMessageId: Int? = null,
|
||||||
|
val rev: Boolean? = null,
|
||||||
|
val fields: String? = null,
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
val map
|
||||||
|
get() = mutableMapOf(
|
||||||
|
"peer_id" to peerId.toString()
|
||||||
|
).apply {
|
||||||
|
count?.let { this["count"] = it.toString() }
|
||||||
|
offset?.let { this["offset"] = it.toString() }
|
||||||
|
extended?.let { this["extended"] = it.intString }
|
||||||
|
startMessageId?.let { this["start_message_id"] = it.toString() }
|
||||||
|
rev?.let { this["rev"] = it.intString }
|
||||||
|
fields?.let { this["fields"] = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class MessagesSendRequest(
|
||||||
|
val peerId: Int,
|
||||||
|
val randomId: Int = 0,
|
||||||
|
val message: String? = null,
|
||||||
|
val lat: Int? = null,
|
||||||
|
val lon: Int? = null,
|
||||||
|
val replyTo: Int? = null,
|
||||||
|
val stickerId: Int? = null,
|
||||||
|
val disableMentions: Boolean? = null,
|
||||||
|
val dontParseLinks: Boolean? = null
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
val map
|
||||||
|
get() = mutableMapOf(
|
||||||
|
"peer_id" to peerId.toString(),
|
||||||
|
"random_id" to randomId.toString()
|
||||||
|
).apply {
|
||||||
|
message?.let { this["message"] = it }
|
||||||
|
lat?.let { this["lat"] = it.toString() }
|
||||||
|
lon?.let { this["lon"] = it.toString() }
|
||||||
|
replyTo?.let { this["reply_to"] = it.toString() }
|
||||||
|
stickerId?.let { this["sticker_id"] = it.toString() }
|
||||||
|
disableMentions?.let { this["disable_mentions"] = it.intString }
|
||||||
|
dontParseLinks?.let { this["dont_parse_links"] = it.intString }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class MessagesMarkAsImportantRequest(
|
||||||
|
val messagesIds: List<Int>,
|
||||||
|
val important: Boolean
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
val map
|
||||||
|
get() = mutableMapOf(
|
||||||
|
"message_ids" to messagesIds.joinToString { it.toString() },
|
||||||
|
"important" to important.intString
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class MessagesGetLongPollServerRequest(
|
||||||
|
val needPts: Boolean,
|
||||||
|
val version: Int
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
val map
|
||||||
|
get() = mutableMapOf(
|
||||||
|
"need_pts" to needPts.intString,
|
||||||
|
"version" to version.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class MessagesPinMessageRequest(
|
||||||
|
val peerId: Int,
|
||||||
|
val messageId: Int? = null,
|
||||||
|
val conversationMessageId: Int? = null
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
val map
|
||||||
|
get() = mutableMapOf(
|
||||||
|
"peer_id" to peerId.toString()
|
||||||
|
).apply {
|
||||||
|
messageId?.let { this["message_id"] = it.toString() }
|
||||||
|
conversationMessageId?.let { this["conversation_message_id"] = it.toString() }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class MessagesUnPinMessageRequest(val peerId: Int) : Parcelable {
|
||||||
|
val map get() = mutableMapOf("peer_id" to peerId.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class MessagesDeleteRequest(
|
||||||
|
val peerId: Int,
|
||||||
|
val messagesIds: List<Int>? = null,
|
||||||
|
val conversationsMessagesIds: List<Int>? = null,
|
||||||
|
val isSpam: Boolean? = null,
|
||||||
|
val deleteForAll: Boolean? = null
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
val map
|
||||||
|
get() = mutableMapOf(
|
||||||
|
"peer_id" to peerId.toString()
|
||||||
|
).apply {
|
||||||
|
isSpam?.let { this["spam"] = it.intString }
|
||||||
|
deleteForAll?.let { this["delete_for_all"] = it.intString }
|
||||||
|
messagesIds?.let {
|
||||||
|
this["message_ids"] = it.joinToString { id -> id.toString() }
|
||||||
|
}
|
||||||
|
|
||||||
|
conversationsMessagesIds?.let {
|
||||||
|
this["conversation_message_ids"] = it.joinToString { id -> id.toString() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class MessagesEditRequest(
|
||||||
|
val peerId: Int,
|
||||||
|
val messageId: Int,
|
||||||
|
val message: String? = null,
|
||||||
|
val lat: Float? = null,
|
||||||
|
val lon: Float? = null,
|
||||||
|
val attachments: List<VkAttachment>? = null,
|
||||||
|
val notParseLinks: Boolean = false,
|
||||||
|
val keepSnippets: Boolean = true,
|
||||||
|
val keepForwardedMessages: Boolean = true
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
val map
|
||||||
|
get() = mutableMapOf(
|
||||||
|
"peer_id" to peerId.toString(),
|
||||||
|
"message_id" to messageId.toString(),
|
||||||
|
"dont_parse_links" to notParseLinks.intString,
|
||||||
|
"keep_snippets" to keepSnippets.intString,
|
||||||
|
"keep_forward_messages" to keepForwardedMessages.intString
|
||||||
|
).apply {
|
||||||
|
message?.let { this["message"] = it }
|
||||||
|
lat?.let { this["lat"] = it.toString() }
|
||||||
|
lon?.let { this["lon"] = it.toString() }
|
||||||
|
attachments?.let {
|
||||||
|
val attachments =
|
||||||
|
if (it.isEmpty()) ""
|
||||||
|
else it.joinToString(separator = ",") { attachment -> attachment.asString() }
|
||||||
|
this["attachment"] = attachments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.meloda.fast.api.model.response
|
package com.meloda.fast.api.network.messages
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.meloda.fast.api.model.base.BaseVkConversation
|
import com.meloda.fast.api.model.base.BaseVkConversation
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.meloda.fast.api.network.repo
|
|
||||||
|
|
||||||
import com.meloda.fast.api.network.VKUrls
|
|
||||||
import com.meloda.fast.api.model.response.ResponseAuthDirect
|
|
||||||
import com.meloda.fast.api.network.Answer
|
|
||||||
import com.meloda.fast.api.model.response.ResponseSendSms
|
|
||||||
import retrofit2.http.*
|
|
||||||
|
|
||||||
interface AuthRepo {
|
|
||||||
|
|
||||||
@GET(VKUrls.Auth.DirectAuth)
|
|
||||||
suspend fun auth(@QueryMap param: Map<String, String?>): Answer<ResponseAuthDirect>
|
|
||||||
|
|
||||||
@GET(VKUrls.Auth.SendSms)
|
|
||||||
suspend fun sendSms(@Query("sid") validationSid: String): Answer<ResponseSendSms>
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.meloda.fast.api.network.repo
|
|
||||||
|
|
||||||
import com.meloda.fast.api.base.ApiResponse
|
|
||||||
import com.meloda.fast.api.network.Answer
|
|
||||||
import com.meloda.fast.api.network.VKUrls
|
|
||||||
import com.meloda.fast.api.model.response.ConversationsGetResponse
|
|
||||||
import retrofit2.http.FieldMap
|
|
||||||
import retrofit2.http.FormUrlEncoded
|
|
||||||
import retrofit2.http.POST
|
|
||||||
|
|
||||||
interface ConversationsRepo {
|
|
||||||
|
|
||||||
@FormUrlEncoded
|
|
||||||
@POST(VKUrls.Conversations.Get)
|
|
||||||
suspend fun getAllChats(@FieldMap params: Map<String, String>): Answer<ApiResponse<ConversationsGetResponse>>
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package com.meloda.fast.api.network.repo
|
|
||||||
|
|
||||||
import com.meloda.fast.api.base.ApiResponse
|
|
||||||
import com.meloda.fast.api.model.base.BaseVkLongPoll
|
|
||||||
import com.meloda.fast.api.model.response.MessagesGetHistoryResponse
|
|
||||||
import com.meloda.fast.api.network.Answer
|
|
||||||
import com.meloda.fast.api.network.VKUrls
|
|
||||||
import retrofit2.http.FieldMap
|
|
||||||
import retrofit2.http.FormUrlEncoded
|
|
||||||
import retrofit2.http.POST
|
|
||||||
|
|
||||||
interface MessagesRepo {
|
|
||||||
|
|
||||||
@FormUrlEncoded
|
|
||||||
@POST(VKUrls.Messages.GetHistory)
|
|
||||||
suspend fun getHistory(@FieldMap params: Map<String, String>): Answer<ApiResponse<MessagesGetHistoryResponse>>
|
|
||||||
|
|
||||||
@FormUrlEncoded
|
|
||||||
@POST(VKUrls.Messages.Send)
|
|
||||||
suspend fun send(@FieldMap params: Map<String, String>): Answer<ApiResponse<Int>>
|
|
||||||
|
|
||||||
@FormUrlEncoded
|
|
||||||
@POST(VKUrls.Messages.MarkAsImportant)
|
|
||||||
suspend fun markAsImportant(@FieldMap params: Map<String, String>): Answer<ApiResponse<List<Int>>>
|
|
||||||
|
|
||||||
@FormUrlEncoded
|
|
||||||
@POST(VKUrls.Messages.GetLongPollServer)
|
|
||||||
suspend fun getLongPollServer(@FieldMap params: Map<String, String>): Answer<ApiResponse<BaseVkLongPoll>>
|
|
||||||
|
|
||||||
}
|
|
||||||
+1
-3
@@ -1,8 +1,6 @@
|
|||||||
package com.meloda.fast.api.network.datasource
|
package com.meloda.fast.api.network.users
|
||||||
|
|
||||||
import com.meloda.fast.api.model.VkUser
|
import com.meloda.fast.api.model.VkUser
|
||||||
import com.meloda.fast.api.network.repo.UsersRepo
|
|
||||||
import com.meloda.fast.api.model.request.UsersGetRequest
|
|
||||||
import com.meloda.fast.database.dao.UsersDao
|
import com.meloda.fast.database.dao.UsersDao
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
package com.meloda.fast.api.network.repo
|
package com.meloda.fast.api.network.users
|
||||||
|
|
||||||
import com.meloda.fast.api.base.ApiResponse
|
import com.meloda.fast.api.base.ApiResponse
|
||||||
import com.meloda.fast.api.model.base.BaseVkUser
|
import com.meloda.fast.api.model.base.BaseVkUser
|
||||||
import com.meloda.fast.api.network.Answer
|
import com.meloda.fast.api.network.Answer
|
||||||
import com.meloda.fast.api.network.VKUrls
|
import com.meloda.fast.api.network.VkUrls
|
||||||
import retrofit2.http.FieldMap
|
import retrofit2.http.FieldMap
|
||||||
import retrofit2.http.FormUrlEncoded
|
import retrofit2.http.FormUrlEncoded
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
@@ -11,7 +11,7 @@ import retrofit2.http.POST
|
|||||||
interface UsersRepo {
|
interface UsersRepo {
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST(VKUrls.Users.GetById)
|
@POST(VkUrls.Users.GetById)
|
||||||
suspend fun getById(
|
suspend fun getById(
|
||||||
@FieldMap params: Map<String, String>?
|
@FieldMap params: Map<String, String>?
|
||||||
): Answer<ApiResponse<List<BaseVkUser>>>
|
): Answer<ApiResponse<List<BaseVkUser>>>
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.meloda.fast.api.model.request
|
package com.meloda.fast.api.network.users
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
package com.meloda.fast.api.network.users
|
||||||
|
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
package com.meloda.fast.base
|
package com.meloda.fast.base
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
|
||||||
import androidx.annotation.LayoutRes
|
import androidx.annotation.LayoutRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.LifecycleRegistry
|
import androidx.lifecycle.LifecycleRegistry
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
|
|
||||||
abstract class BaseActivity : AppCompatActivity, LifecycleOwner {
|
abstract class BaseActivity : AppCompatActivity, LifecycleOwner {
|
||||||
|
|
||||||
@@ -39,10 +37,4 @@ abstract class BaseActivity : AppCompatActivity, LifecycleOwner {
|
|||||||
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
|
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
|
||||||
}
|
}
|
||||||
|
|
||||||
val rootView: View? get() = findViewById(android.R.id.content)
|
|
||||||
|
|
||||||
fun requireRootView() = rootView!!
|
|
||||||
|
|
||||||
var errorSnackbar: Snackbar? = null
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ import com.meloda.fast.activity.MainActivity
|
|||||||
import com.meloda.fast.api.UserConfig
|
import com.meloda.fast.api.UserConfig
|
||||||
import com.meloda.fast.base.viewmodel.BaseViewModel
|
import com.meloda.fast.base.viewmodel.BaseViewModel
|
||||||
import com.meloda.fast.base.viewmodel.IllegalTokenEvent
|
import com.meloda.fast.base.viewmodel.IllegalTokenEvent
|
||||||
import com.meloda.fast.base.viewmodel.VKEvent
|
import com.meloda.fast.base.viewmodel.VkEvent
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ abstract class BaseViewModelFragment<VM : BaseViewModel> : BaseFragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun onEvent(event: VKEvent) {
|
protected open fun onEvent(event: VkEvent) {
|
||||||
if (event is IllegalTokenEvent) {
|
if (event is IllegalTokenEvent) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
requireContext(), R.string.authorization_failed, Toast.LENGTH_LONG
|
requireContext(), R.string.authorization_failed, Toast.LENGTH_LONG
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ abstract class BaseAdapter<Item, VH : BaseHolder>(
|
|||||||
holder.bind(position)
|
holder.bind(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun initListeners(itemView: View, position: Int) {
|
protected open fun initListeners(itemView: View, position: Int) {
|
||||||
if (itemView is AdapterView<*>) return
|
if (itemView is AdapterView<*>) return
|
||||||
|
|
||||||
itemView.setOnClickListener { itemClickListener.invoke(position) }
|
itemView.setOnClickListener { itemClickListener.invoke(position) }
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ abstract class BaseViewModel : ViewModel() {
|
|||||||
|
|
||||||
var unknownErrorDefaultText: String = ""
|
var unknownErrorDefaultText: String = ""
|
||||||
|
|
||||||
protected val tasksEventChannel = Channel<VKEvent>()
|
protected val tasksEventChannel = Channel<VkEvent>()
|
||||||
val tasksEvent = tasksEventChannel.receiveAsFlow()
|
val tasksEvent = tasksEventChannel.receiveAsFlow()
|
||||||
|
|
||||||
protected fun <T> makeJob(
|
protected fun <T> makeJob(
|
||||||
@@ -25,23 +25,35 @@ abstract class BaseViewModel : ViewModel() {
|
|||||||
onEnd: (suspend () -> Unit)? = null,
|
onEnd: (suspend () -> Unit)? = null,
|
||||||
onError: (suspend (Throwable) -> Unit)? = null
|
onError: (suspend (Throwable) -> Unit)? = null
|
||||||
) = viewModelScope.launch {
|
) = viewModelScope.launch {
|
||||||
onStart?.invoke()
|
onStart?.invoke() ?: onStart()
|
||||||
when (val response = job()) {
|
when (val response = job()) {
|
||||||
is Answer.Success -> onAnswer(response.data)
|
is Answer.Success -> onAnswer(response.data)
|
||||||
is Answer.Error -> {
|
is Answer.Error -> {
|
||||||
checkErrors(response.throwable)
|
checkErrors(response.throwable)
|
||||||
onError?.invoke(response.throwable)
|
onError?.invoke(response.throwable) ?: onError(response.throwable)
|
||||||
?: sendEvent(
|
}
|
||||||
ErrorEvent(
|
}
|
||||||
response.throwable.message
|
}.also {
|
||||||
?: unknownErrorDefaultText
|
it.invokeOnCompletion {
|
||||||
)
|
viewModelScope.launch {
|
||||||
)
|
onEnd?.invoke() ?: onStop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.also { it.invokeOnCompletion { viewModelScope.launch { onEnd?.invoke() } } }
|
|
||||||
|
|
||||||
protected suspend fun <T : VKEvent> sendEvent(event: T) = tasksEventChannel.send(event)
|
protected suspend fun onStart() {
|
||||||
|
sendEvent(StartProgressEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected suspend fun onStop() {
|
||||||
|
sendEvent(StopProgressEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected suspend fun onError(throwable: Throwable) {
|
||||||
|
sendEvent(ErrorEvent(throwable.message ?: unknownErrorDefaultText))
|
||||||
|
}
|
||||||
|
|
||||||
|
protected suspend fun <T : VkEvent> sendEvent(event: T) = tasksEventChannel.send(event)
|
||||||
|
|
||||||
private suspend fun checkErrors(throwable: Throwable) {
|
private suspend fun checkErrors(throwable: Throwable) {
|
||||||
when (throwable) {
|
when (throwable) {
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ data class ShowDialogInfoEvent(
|
|||||||
val message: String,
|
val message: String,
|
||||||
val positiveBtn: String? = null,
|
val positiveBtn: String? = null,
|
||||||
val negativeBtn: String? = null
|
val negativeBtn: String? = null
|
||||||
) : VKEvent()
|
) : VkEvent()
|
||||||
|
|
||||||
data class ErrorEvent(val errorText: String) : VKEvent()
|
data class ErrorEvent(val errorText: String) : VkEvent()
|
||||||
|
|
||||||
object IllegalTokenEvent : VKEvent()
|
object IllegalTokenEvent : VkEvent()
|
||||||
data class CaptchaEvent(val sid: String, val image: String) : VKEvent()
|
data class CaptchaEvent(val sid: String, val image: String) : VkEvent()
|
||||||
data class ValidationEvent(val sid: String) : VKEvent()
|
data class ValidationEvent(val sid: String) : VkEvent()
|
||||||
|
|
||||||
object StartProgressEvent : VKEvent()
|
object StartProgressEvent : VkEvent()
|
||||||
object StopProgressEvent : VKEvent()
|
object StopProgressEvent : VkEvent()
|
||||||
|
|
||||||
abstract class VKEvent
|
abstract class VkEvent
|
||||||
@@ -13,6 +13,7 @@ import kotlinx.coroutines.Job
|
|||||||
object AppSettings {
|
object AppSettings {
|
||||||
|
|
||||||
val keyIsMultilineEnabled = booleanPreferencesKey("isMultilineEnabled")
|
val keyIsMultilineEnabled = booleanPreferencesKey("isMultilineEnabled")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
|
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import com.meloda.fast.database.dao.UsersDao
|
|||||||
VkUser::class,
|
VkUser::class,
|
||||||
VkGroup::class
|
VkGroup::class
|
||||||
],
|
],
|
||||||
version = 24,
|
version = 26,
|
||||||
exportSchema = false,
|
exportSchema = false,
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ import com.google.gson.Gson
|
|||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.meloda.fast.api.network.AuthInterceptor
|
import com.meloda.fast.api.network.AuthInterceptor
|
||||||
import com.meloda.fast.api.network.ResultCallFactory
|
import com.meloda.fast.api.network.ResultCallFactory
|
||||||
import com.meloda.fast.api.network.datasource.AuthDataSource
|
import com.meloda.fast.api.network.auth.AuthRepo
|
||||||
import com.meloda.fast.api.network.datasource.ConversationsDataSource
|
import com.meloda.fast.api.network.auth.AuthDataSource
|
||||||
import com.meloda.fast.api.network.datasource.MessagesDataSource
|
import com.meloda.fast.api.network.conversations.ConversationsDataSource
|
||||||
import com.meloda.fast.api.network.datasource.UsersDataSource
|
import com.meloda.fast.api.network.conversations.ConversationsRepo
|
||||||
import com.meloda.fast.api.network.repo.*
|
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.UsersRepo
|
||||||
import com.meloda.fast.database.dao.ConversationsDao
|
import com.meloda.fast.database.dao.ConversationsDao
|
||||||
import com.meloda.fast.database.dao.MessagesDao
|
import com.meloda.fast.database.dao.MessagesDao
|
||||||
import com.meloda.fast.database.dao.UsersDao
|
import com.meloda.fast.database.dao.UsersDao
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.meloda.fast.extensions
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.annotation.*
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.res.ResourcesCompat
|
|
||||||
|
|
||||||
object ContextExtensions {
|
|
||||||
|
|
||||||
fun Context.drawable(@DrawableRes resId: Int): Drawable? {
|
|
||||||
return ContextCompat.getDrawable(this, resId)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ColorInt
|
|
||||||
fun Context.color(@ColorRes resId: Int): Int {
|
|
||||||
return ContextCompat.getColor(this, resId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.font(@FontRes resId: Int): Typeface? {
|
|
||||||
return ResourcesCompat.getFont(this, resId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.string(@StringRes resId: Int): String {
|
|
||||||
return getString(resId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Context.view(resId: Int, root: ViewGroup? = null, attachToRoot: Boolean = false): View {
|
|
||||||
return LayoutInflater.from(this).inflate(resId, root, attachToRoot)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.meloda.fast.extensions
|
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import androidx.annotation.ColorInt
|
|
||||||
|
|
||||||
object DrawableExtensions {
|
|
||||||
|
|
||||||
fun Drawable?.tint(@ColorInt color: Int): Drawable? {
|
|
||||||
this?.setTint(color)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package com.meloda.fast.extensions
|
|
||||||
|
|
||||||
import android.graphics.*
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
fun Bitmap.borderedCircularBitmap(
|
|
||||||
borderColor: Int = 0,
|
|
||||||
borderWidth: Int = 0
|
|
||||||
): Bitmap? {
|
|
||||||
val bitmap = Bitmap.createBitmap(
|
|
||||||
width, // width in pixels
|
|
||||||
height, // height in pixels
|
|
||||||
Bitmap.Config.ARGB_8888
|
|
||||||
)
|
|
||||||
|
|
||||||
// canvas to draw circular bitmap
|
|
||||||
val canvas = Canvas(bitmap)
|
|
||||||
|
|
||||||
// get the maximum radius
|
|
||||||
val radius = min(width / 2f, height / 2f)
|
|
||||||
|
|
||||||
// create a path to draw circular bitmap border
|
|
||||||
val borderPath = Path().apply {
|
|
||||||
addCircle(
|
|
||||||
width / 2f,
|
|
||||||
height / 2f,
|
|
||||||
radius,
|
|
||||||
Path.Direction.CCW
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw border on circular bitmap
|
|
||||||
canvas.clipPath(borderPath)
|
|
||||||
canvas.drawColor(borderColor)
|
|
||||||
|
|
||||||
|
|
||||||
// create a path for circular bitmap
|
|
||||||
val bitmapPath = Path().apply {
|
|
||||||
addCircle(
|
|
||||||
width / 2f,
|
|
||||||
height / 2f,
|
|
||||||
radius - borderWidth,
|
|
||||||
Path.Direction.CCW
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.clipPath(bitmapPath)
|
|
||||||
val paint = Paint().apply {
|
|
||||||
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
|
|
||||||
isAntiAlias = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear the circular bitmap drawing area
|
|
||||||
// it will keep bitmap transparency
|
|
||||||
canvas.drawBitmap(this, 0f, 0f, paint)
|
|
||||||
|
|
||||||
// now draw the circular bitmap
|
|
||||||
canvas.drawBitmap(this, 0f, 0f, null)
|
|
||||||
|
|
||||||
|
|
||||||
val diameter = (radius * 2).toInt()
|
|
||||||
val x = (width - diameter) / 2
|
|
||||||
val y = (height - diameter) / 2
|
|
||||||
|
|
||||||
// return cropped circular bitmap with border
|
|
||||||
return Bitmap.createBitmap(
|
|
||||||
bitmap, // source bitmap
|
|
||||||
x, // x coordinate of the first pixel in source
|
|
||||||
y, // y coordinate of the first pixel in source
|
|
||||||
diameter, // width
|
|
||||||
diameter // height
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.meloda.fast.extensions
|
|
||||||
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
object FloatExtensions {
|
|
||||||
|
|
||||||
fun Float.int(): Int {
|
|
||||||
return roundToInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
package com.meloda.fast.extensions
|
|
||||||
|
|
||||||
import androidx.annotation.UiThread
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
|
|
||||||
object LiveDataExtensions {
|
|
||||||
|
|
||||||
operator fun <T> MutableLiveData<MutableList<T>>.set(position: Int, v: T) {
|
|
||||||
val value = (this.value ?: arrayListOf()).apply { this[position] = v }
|
|
||||||
this.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun <T> MutableLiveData<MutableList<T>>.get(position: Int): T {
|
|
||||||
return (value as MutableList<T>)[position]
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
fun <T> MutableLiveData<MutableList<T>>.add(v: T, position: Int = -1) {
|
|
||||||
val value = (this.value ?: arrayListOf()).apply {
|
|
||||||
if (position == -1) this.add(v) else this.add(position, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
fun <T> MutableLiveData<MutableList<T>>.addAll(values: List<T>, position: Int = -1) {
|
|
||||||
val value = (this.value ?: arrayListOf()).apply {
|
|
||||||
if (position == -1) this.addAll(values)
|
|
||||||
else this.addAll(position, values)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("TYPE_INFERENCE_ONLY_INPUT_TYPES_WARNING")
|
|
||||||
fun <T> MutableLiveData<MutableList<T>>.removeAll(values: List<T>) {
|
|
||||||
val value = (this.value ?: arrayListOf()).apply {
|
|
||||||
this.removeAll(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> MutableLiveData<MutableList<T>>.removeAt(index: Int) {
|
|
||||||
val value = (this.value ?: arrayListOf()).apply {
|
|
||||||
this.removeAt(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> MutableLiveData<MutableList<T>>.remove(item: T) {
|
|
||||||
val value = (this.value ?: arrayListOf()).apply {
|
|
||||||
this.remove(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun <T> MutableLiveData<MutableList<T>>.iterator(): Iterator<T> {
|
|
||||||
return (value as MutableList<T>).iterator()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> MutableLiveData<MutableList<T>>.clear() {
|
|
||||||
value = arrayListOf()
|
|
||||||
}
|
|
||||||
|
|
||||||
val <T> MutableLiveData<MutableList<T>>.indices get() = (value as MutableList<T>).indices
|
|
||||||
|
|
||||||
val <T> MutableLiveData<MutableList<T>>.size get() = (value as MutableList<T>).size
|
|
||||||
|
|
||||||
fun <T> MutableLiveData<MutableList<T>>.isEmpty(): Boolean {
|
|
||||||
return (value as MutableList<T>).isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> MutableLiveData<MutableList<T>>.isNotEmpty(): Boolean {
|
|
||||||
return !isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> MutableLiveData<MutableList<T>>.requireValue() = value!!
|
|
||||||
|
|
||||||
@UiThread
|
|
||||||
operator fun <T> MutableLiveData<MutableList<T>>.plusAssign(values: List<T>) {
|
|
||||||
val value = (this.value ?: arrayListOf()).apply {
|
|
||||||
this.addAll(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun <T> MutableLiveData<MutableList<T>>.plusAssign(v: T) {
|
|
||||||
val value = (this.value ?: arrayListOf()).apply {
|
|
||||||
this.add(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun <T> MutableLiveData<MutableList<T>>.minusAssign(values: List<T>) {
|
|
||||||
val value = (this.value ?: arrayListOf()).apply {
|
|
||||||
this.removeAll(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun <T> MutableLiveData<MutableList<T>>.minusAssign(v: T) {
|
|
||||||
val value = (this.value ?: arrayListOf()).apply {
|
|
||||||
this.remove(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.meloda.fast.extensions
|
|
||||||
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
object StringExtensions {
|
|
||||||
|
|
||||||
fun String.lowerCase(): String {
|
|
||||||
return toLowerCase(Locale.getDefault())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
package com.meloda.fast.extensions
|
package com.meloda.fast.extensions
|
||||||
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
|
|
||||||
object TextViewExtensions {
|
object TextViewExtensions {
|
||||||
|
|
||||||
fun TextView.clear() {
|
fun TextView.clear() {
|
||||||
text = ""
|
text = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TextInputLayout.clear() {
|
|
||||||
editText?.setText("")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.meloda.fast.io
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
|
|
||||||
class BytesOutputStream : ByteArrayOutputStream {
|
|
||||||
constructor() : super(8192)
|
|
||||||
constructor(size: Int) : super(size)
|
|
||||||
|
|
||||||
val byteArray: ByteArray = buf
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.meloda.fast.io
|
|
||||||
|
|
||||||
import java.nio.charset.Charset
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
|
|
||||||
object Charsets {
|
|
||||||
|
|
||||||
val ASCII: Charset = StandardCharsets.US_ASCII
|
|
||||||
|
|
||||||
val UTF_8: Charset = StandardCharsets.UTF_8
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
package com.meloda.fast.io
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.Contract
|
|
||||||
import java.io.*
|
|
||||||
import java.nio.charset.Charset
|
|
||||||
import java.util.zip.GZIPInputStream
|
|
||||||
import java.util.zip.GZIPOutputStream
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
object EasyStreams {
|
|
||||||
|
|
||||||
const val BUFFER_SIZE = 8192
|
|
||||||
const val CHAR_BUFFER_SIZE = 4096
|
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun read(from: InputStream, encoding: Charset? = Charsets.UTF_8): String {
|
|
||||||
return read(InputStreamReader(from, encoding))
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun read(from: Reader): String {
|
|
||||||
val builder = StringWriter(CHAR_BUFFER_SIZE)
|
|
||||||
return try {
|
|
||||||
copy(from, builder)
|
|
||||||
builder.toString()
|
|
||||||
} finally {
|
|
||||||
close(from)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun readBytes(from: InputStream): ByteArray {
|
|
||||||
val output = ByteArrayOutputStream(max(from.available(), BUFFER_SIZE))
|
|
||||||
try {
|
|
||||||
copy(from, output)
|
|
||||||
} finally {
|
|
||||||
close(from)
|
|
||||||
}
|
|
||||||
return output.toByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun write(from: ByteArray?, to: OutputStream) {
|
|
||||||
try {
|
|
||||||
to.write(from)
|
|
||||||
to.flush()
|
|
||||||
} finally {
|
|
||||||
close(to)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun write(from: String?, to: OutputStream?) {
|
|
||||||
write(from, OutputStreamWriter(to, Charsets.UTF_8))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun write(from: CharArray?, to: Writer) {
|
|
||||||
try {
|
|
||||||
to.write(from)
|
|
||||||
to.flush()
|
|
||||||
} finally {
|
|
||||||
close(to)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun write(from: String?, to: Writer) {
|
|
||||||
try {
|
|
||||||
to.write(from)
|
|
||||||
to.flush()
|
|
||||||
} finally {
|
|
||||||
close(to)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun copy(from: Reader, to: Writer): Long {
|
|
||||||
val buffer = CharArray(CHAR_BUFFER_SIZE)
|
|
||||||
var read: Int
|
|
||||||
var total: Long = 0
|
|
||||||
while (from.read(buffer).also { read = it } != -1) {
|
|
||||||
to.write(buffer, 0, read)
|
|
||||||
total += read.toLong()
|
|
||||||
}
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun copy(from: InputStream, to: OutputStream): Long {
|
|
||||||
val buffer = ByteArray(BUFFER_SIZE)
|
|
||||||
var read: Int
|
|
||||||
var total: Long = 0
|
|
||||||
while (from.read(buffer).also { read = it } != -1) {
|
|
||||||
to.write(buffer, 0, read)
|
|
||||||
total += read.toLong()
|
|
||||||
}
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|
||||||
fun buffer(input: InputStream?): BufferedInputStream {
|
|
||||||
return buffer(input, BUFFER_SIZE)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Contract("null, _ -> new")
|
|
||||||
fun buffer(input: InputStream?, size: Int): BufferedInputStream {
|
|
||||||
return if (input is BufferedInputStream) input else BufferedInputStream(input, size)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun buffer(output: OutputStream?): BufferedOutputStream {
|
|
||||||
return buffer(output, BUFFER_SIZE)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Contract("null, _ -> new")
|
|
||||||
fun buffer(output: OutputStream?, size: Int): BufferedOutputStream {
|
|
||||||
return if (output is BufferedOutputStream) output else BufferedOutputStream(output, size)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun buffer(input: Reader?): BufferedReader {
|
|
||||||
return buffer(input, CHAR_BUFFER_SIZE)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Contract("null, _ -> new")
|
|
||||||
fun buffer(input: Reader?, size: Int): BufferedReader {
|
|
||||||
return if (input is BufferedReader) input else BufferedReader(input, size)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun buffer(output: Writer?): BufferedWriter {
|
|
||||||
return buffer(output, CHAR_BUFFER_SIZE)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Contract("null, _ -> new")
|
|
||||||
fun buffer(output: Writer?, size: Int): BufferedWriter {
|
|
||||||
return if (output is BufferedWriter) output else BufferedWriter(output, size)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun gzip(input: InputStream?): GZIPInputStream {
|
|
||||||
return gzip(input, BUFFER_SIZE)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Contract("null, _ -> new")
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun gzip(input: InputStream?, size: Int): GZIPInputStream {
|
|
||||||
return if (input is GZIPInputStream) input else GZIPInputStream(input, size)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun gzip(input: OutputStream?): GZIPOutputStream {
|
|
||||||
return gzip(input, BUFFER_SIZE)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Contract("null, _ -> new")
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun gzip(input: OutputStream?, size: Int): GZIPOutputStream {
|
|
||||||
return if (input is GZIPOutputStream) input else GZIPOutputStream(input, size)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun close(c: Closeable?): Boolean {
|
|
||||||
if (c != null) {
|
|
||||||
try {
|
|
||||||
c.close()
|
|
||||||
return true
|
|
||||||
} catch (e: IOException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package com.meloda.fast.io
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.Contract
|
|
||||||
import java.io.*
|
|
||||||
import java.math.BigInteger
|
|
||||||
|
|
||||||
object FileStreams {
|
|
||||||
|
|
||||||
val lineSeparatorChar = lineSeparator()[0]
|
|
||||||
|
|
||||||
const val ONE_KB = 1024
|
|
||||||
const val ONE_MB = ONE_KB * 1024
|
|
||||||
const val ONE_GB = ONE_MB * 1024
|
|
||||||
const val ONE_TB = ONE_GB * 1024L
|
|
||||||
const val ONE_PB = ONE_TB * 1024L
|
|
||||||
const val ONE_EB = ONE_PB * 1024L
|
|
||||||
|
|
||||||
val ONE_ZB: BigInteger = BigInteger.valueOf(ONE_EB).multiply(BigInteger.valueOf(1024L))
|
|
||||||
val ONE_YB: BigInteger = ONE_ZB.multiply(BigInteger.valueOf(1024L))
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun read(from: File?): String {
|
|
||||||
return EasyStreams.read(reader(from))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun write(from: String?, to: File?) {
|
|
||||||
EasyStreams.write(from, writer(to))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun write(from: ByteArray?, to: File?) {
|
|
||||||
EasyStreams.write(from, FileOutputStream(to))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun append(from: ByteArray?, to: File?) {
|
|
||||||
EasyStreams.write(from, FileOutputStream(to, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun append(from: CharArray?, to: File?) {
|
|
||||||
EasyStreams.write(from, FileWriter(to, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun append(from: CharSequence, to: File?) {
|
|
||||||
EasyStreams.write(if (from is String) from else from.toString(), FileWriter(to, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun delete(dir: File) {
|
|
||||||
if (dir.isDirectory) {
|
|
||||||
val files = dir.listFiles() ?: return
|
|
||||||
for (file in files) {
|
|
||||||
delete(file)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dir.delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun lineSeparator(): String {
|
|
||||||
return System.lineSeparator()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun search(dir: File, name: String?): File? {
|
|
||||||
require(dir.isDirectory) { "dir can't be file." }
|
|
||||||
|
|
||||||
val files = dir.listFiles() ?: return null
|
|
||||||
|
|
||||||
if (files.isEmpty()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
for (file in files) {
|
|
||||||
if (file.isDirectory) {
|
|
||||||
search(file, name)
|
|
||||||
} else if (file.name.contains(name!!)) {
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
@Contract("_ -> new")
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun reader(from: File?): Reader {
|
|
||||||
return InputStreamReader(FileInputStream(from), Charsets.UTF_8)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Contract("_ -> new")
|
|
||||||
@Throws(FileNotFoundException::class)
|
|
||||||
fun writer(to: File?): Writer {
|
|
||||||
return OutputStreamWriter(FileOutputStream(to), Charsets.UTF_8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -75,29 +75,17 @@ class ConversationsAdapter constructor(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val chatUser: VkUser? = if (conversation.isUser()) {
|
val conversationUser = VkUtils.getConversationUser(conversation, profiles)
|
||||||
profiles[conversation.id]
|
val conversationGroup = VkUtils.getConversationGroup(conversation, groups)
|
||||||
} else null
|
|
||||||
|
|
||||||
val messageUser: VkUser? = if (message.isUser()) {
|
val messageUser = VkUtils.getMessageUser(message, profiles)
|
||||||
profiles[message.fromId]
|
val messageGroup = VkUtils.getMessageGroup(message, groups)
|
||||||
} else null
|
|
||||||
|
|
||||||
val chatGroup: VkGroup? = if (conversation.isGroup()) {
|
val avatar = VkUtils.getConversationAvatar(
|
||||||
groups[conversation.id]
|
conversation = conversation,
|
||||||
} else null
|
conversationUser = conversationUser,
|
||||||
|
conversationGroup = conversationGroup
|
||||||
val messageGroup: VkGroup? = if (message.isGroup()) {
|
)
|
||||||
groups[message.fromId]
|
|
||||||
} else null
|
|
||||||
|
|
||||||
val avatar = when {
|
|
||||||
conversation.ownerId == VKConstants.FAST_GROUP_ID -> null
|
|
||||||
conversation.isUser() && chatUser != null && !chatUser.photo200.isNullOrBlank() -> chatUser.photo200
|
|
||||||
conversation.isGroup() && chatGroup != null && !chatGroup.photo200.isNullOrBlank() -> chatGroup.photo200
|
|
||||||
conversation.isChat() && !conversation.photo200.isNullOrBlank() -> conversation.photo200
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.avatar.isVisible = avatar != null
|
binding.avatar.isVisible = avatar != null
|
||||||
|
|
||||||
@@ -136,7 +124,7 @@ class ConversationsAdapter constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.online.isVisible = chatUser?.online == true
|
binding.online.isVisible = conversationUser?.online == true
|
||||||
|
|
||||||
binding.pin.isVisible = conversation.isPinned
|
binding.pin.isVisible = conversation.isPinned
|
||||||
|
|
||||||
@@ -210,7 +198,8 @@ class ConversationsAdapter constructor(
|
|||||||
binding.message.text = spanMessage
|
binding.message.text = spanMessage
|
||||||
|
|
||||||
binding.title.text =
|
binding.title.text =
|
||||||
getItem(position).title ?: chatUser?.toString() ?: chatGroup?.name ?: "..."
|
getItem(position).title ?: conversationUser?.toString() ?: conversationGroup?.name
|
||||||
|
?: "..."
|
||||||
|
|
||||||
binding.date.text = TimeUtils.getLocalizedTime(context, message.date * 1000L)
|
binding.date.text = TimeUtils.getLocalizedTime(context, message.date * 1000L)
|
||||||
|
|
||||||
@@ -232,6 +221,18 @@ class ConversationsAdapter constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeConversation(conversationId: Int): Int? {
|
||||||
|
for (i in values.indices) {
|
||||||
|
val conversation = values[i]
|
||||||
|
if (conversation.id == conversationId) {
|
||||||
|
values.removeAt(i)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val COMPARATOR = object : DiffUtil.ItemCallback<VkConversation>() {
|
private val COMPARATOR = object : DiffUtil.ItemCallback<VkConversation>() {
|
||||||
override fun areItemsTheSame(
|
override fun areItemsTheSame(
|
||||||
|
|||||||
+180
-37
@@ -1,43 +1,46 @@
|
|||||||
package com.meloda.fast.screens.conversations
|
package com.meloda.fast.screens.conversations
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.Gravity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.viewbinding.library.fragment.viewBinding
|
import android.viewbinding.library.fragment.viewBinding
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import coil.load
|
import coil.load
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.meloda.fast.R
|
import com.meloda.fast.R
|
||||||
|
import com.meloda.fast.activity.MainActivity
|
||||||
import com.meloda.fast.api.UserConfig
|
import com.meloda.fast.api.UserConfig
|
||||||
import com.meloda.fast.api.model.VkConversation
|
import com.meloda.fast.api.model.VkConversation
|
||||||
import com.meloda.fast.base.BaseViewModelFragment
|
import com.meloda.fast.base.BaseViewModelFragment
|
||||||
import com.meloda.fast.base.viewmodel.StartProgressEvent
|
import com.meloda.fast.base.viewmodel.StartProgressEvent
|
||||||
import com.meloda.fast.base.viewmodel.StopProgressEvent
|
import com.meloda.fast.base.viewmodel.StopProgressEvent
|
||||||
import com.meloda.fast.base.viewmodel.VKEvent
|
import com.meloda.fast.base.viewmodel.VkEvent
|
||||||
|
import com.meloda.fast.common.AppGlobal
|
||||||
import com.meloda.fast.common.AppSettings
|
import com.meloda.fast.common.AppSettings
|
||||||
import com.meloda.fast.common.dataStore
|
import com.meloda.fast.common.dataStore
|
||||||
import com.meloda.fast.databinding.FragmentConversationsBinding
|
import com.meloda.fast.databinding.FragmentConversationsBinding
|
||||||
import com.meloda.fast.util.AndroidUtils
|
import com.meloda.fast.util.AndroidUtils
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ConversationsFragment :
|
class ConversationsFragment :
|
||||||
BaseViewModelFragment<ConversationsViewModel>(R.layout.fragment_conversations) {
|
BaseViewModelFragment<ConversationsViewModel>(R.layout.fragment_conversations) {
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "ConversationsFragment"
|
|
||||||
}
|
|
||||||
|
|
||||||
override val viewModel: ConversationsViewModel by viewModels()
|
override val viewModel: ConversationsViewModel by viewModels()
|
||||||
private val binding: FragmentConversationsBinding by viewBinding()
|
private val binding: FragmentConversationsBinding by viewBinding()
|
||||||
|
|
||||||
@@ -53,8 +56,25 @@ class ConversationsFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val avatarPopupMenu: PopupMenu
|
||||||
|
get() =
|
||||||
|
PopupMenu(
|
||||||
|
requireContext(),
|
||||||
|
binding.avatar,
|
||||||
|
Gravity.BOTTOM
|
||||||
|
).apply {
|
||||||
|
menu.add(getString(R.string.log_out))
|
||||||
|
setOnMenuItemClickListener { item ->
|
||||||
|
if (item.title == getString(R.string.log_out)) {
|
||||||
|
showLogOutDialog()
|
||||||
|
return@setOnMenuItemClickListener true
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var isPaused = false
|
private var isPaused = false
|
||||||
private var isExpanded = true
|
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
@@ -71,14 +91,10 @@ class ConversationsFragment :
|
|||||||
requireContext().dataStore.data.map {
|
requireContext().dataStore.data.map {
|
||||||
adapter.isMultilineEnabled = it[AppSettings.keyIsMultilineEnabled] ?: true
|
adapter.isMultilineEnabled = it[AppSettings.keyIsMultilineEnabled] ?: true
|
||||||
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||||
}.collect { }
|
}.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.createChat.setOnClickListener {
|
binding.createChat.setOnClickListener {}
|
||||||
Snackbar.make(it, "Test snackbar", Snackbar.LENGTH_SHORT)
|
|
||||||
.setAction("Action") {}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
UserConfig.vkUser.observe(viewLifecycleOwner) {
|
UserConfig.vkUser.observe(viewLifecycleOwner) {
|
||||||
it?.let { user -> binding.avatar.load(user.photo200) { crossfade(100) } }
|
it?.let { user -> binding.avatar.load(user.photo200) { crossfade(100) } }
|
||||||
@@ -87,28 +103,32 @@ class ConversationsFragment :
|
|||||||
binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
|
binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
|
||||||
if (isPaused) return@OnOffsetChangedListener
|
if (isPaused) return@OnOffsetChangedListener
|
||||||
|
|
||||||
if (verticalOffset <= -100) {
|
binding.appBar.animate().translationZ(
|
||||||
binding.avatarContainer.alpha = 0f
|
if (verticalOffset < 0) AndroidUtils.px(3).roundToInt().toFloat()
|
||||||
return@OnOffsetChangedListener
|
else 0f
|
||||||
}
|
).setDuration(50).start()
|
||||||
|
|
||||||
val alpha = 1 - abs(verticalOffset * 0.01).toFloat()
|
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.avatarContainer.alpha = alpha
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isPaused) {
|
binding.avatar.setOnClickListener { avatarPopupMenu.show() }
|
||||||
isPaused = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.toolbar.overflowIcon = ContextCompat.getDrawable(requireContext(), R.drawable.test)
|
binding.avatar.setOnLongClickListener {
|
||||||
|
lifecycleScope.launch {
|
||||||
viewModel.loadProfileUser()
|
|
||||||
viewModel.loadConversations()
|
|
||||||
|
|
||||||
binding.avatar.setOnClickListener {
|
|
||||||
lifecycleScope.launchWhenResumed {
|
|
||||||
requireContext().dataStore.edit { settings ->
|
requireContext().dataStore.edit { settings ->
|
||||||
val isMultilineEnabled = settings[AppSettings.keyIsMultilineEnabled] ?: true
|
val isMultilineEnabled = settings[AppSettings.keyIsMultilineEnabled] ?: true
|
||||||
settings[AppSettings.keyIsMultilineEnabled] = !isMultilineEnabled
|
settings[AppSettings.keyIsMultilineEnabled] = !isMultilineEnabled
|
||||||
@@ -117,15 +137,59 @@ class ConversationsFragment :
|
|||||||
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEvent(event: VKEvent) {
|
if (isPaused) {
|
||||||
|
isPaused = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.loadProfileUser()
|
||||||
|
viewModel.loadConversations()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLogOutDialog() {
|
||||||
|
val isEasterEgg = UserConfig.userId == UserConfig.userId
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(
|
||||||
|
if (isEasterEgg) "Выйти внаружу?"
|
||||||
|
else getString(R.string.sign_out_confirm_title)
|
||||||
|
)
|
||||||
|
.setMessage(R.string.sign_out_confirm)
|
||||||
|
.setPositiveButton(
|
||||||
|
if (isEasterEgg) "Выйти внаружу"
|
||||||
|
else getString(R.string.action_sign_out)
|
||||||
|
) { _, _ ->
|
||||||
|
lifecycleScope.launch(Dispatchers.Default) {
|
||||||
|
UserConfig.clear()
|
||||||
|
AppGlobal.appDatabase.clearAllTables()
|
||||||
|
|
||||||
|
requireActivity().finishAffinity()
|
||||||
|
requireActivity().startActivity(
|
||||||
|
Intent(
|
||||||
|
requireContext(),
|
||||||
|
MainActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.no, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEvent(event: VkEvent) {
|
||||||
super.onEvent(event)
|
super.onEvent(event)
|
||||||
when (event) {
|
when (event) {
|
||||||
is ConversationsLoaded -> refreshConversations(event)
|
|
||||||
is StartProgressEvent -> onProgressStarted()
|
is StartProgressEvent -> onProgressStarted()
|
||||||
is StopProgressEvent -> onProgressStopped()
|
is StopProgressEvent -> onProgressStopped()
|
||||||
|
|
||||||
|
is ConversationsLoaded -> refreshConversations(event)
|
||||||
|
is ConversationsDelete -> deleteConversation(event.peerId)
|
||||||
|
|
||||||
|
// TODO: 10-Oct-21 remove this and sort conversations list
|
||||||
|
is ConversationsPin, is ConversationsUnpin -> viewModel.loadConversations()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,13 +243,19 @@ class ConversationsFragment :
|
|||||||
private fun fillRecyclerView(values: List<VkConversation>) {
|
private fun fillRecyclerView(values: List<VkConversation>) {
|
||||||
adapter.values.clear()
|
adapter.values.clear()
|
||||||
adapter.values += values
|
adapter.values += values
|
||||||
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
adapter.submitList(values)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onItemClick(position: Int) {
|
private fun onItemClick(position: Int) {
|
||||||
val conversation = adapter[position]
|
val conversation = adapter[position]
|
||||||
val user = if (conversation.isUser()) adapter.profiles[conversation.id] else null
|
|
||||||
val group = if (conversation.isGroup()) adapter.groups[conversation.id] else null
|
val user =
|
||||||
|
if (conversation.isUser()) adapter.profiles[conversation.id]
|
||||||
|
else null
|
||||||
|
|
||||||
|
val group =
|
||||||
|
if (conversation.isGroup()) adapter.groups[conversation.id]
|
||||||
|
else null
|
||||||
|
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
R.id.toMessagesHistory,
|
R.id.toMessagesHistory,
|
||||||
@@ -198,8 +268,81 @@ class ConversationsFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onItemLongClick(position: Int): Boolean {
|
private fun onItemLongClick(position: Int): Boolean {
|
||||||
binding.createChat.performClick()
|
showOptionsDialog(position)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showOptionsDialog(position: Int) {
|
||||||
|
val conversation = adapter[position]
|
||||||
|
|
||||||
|
var canPinOneMoreDialog = true
|
||||||
|
if (adapter.itemCount > 4) {
|
||||||
|
val firstFiveDialogs = adapter.values.subList(0, 5)
|
||||||
|
var pinnedCount = 0
|
||||||
|
|
||||||
|
firstFiveDialogs.forEach { if (it.isPinned) pinnedCount++ }
|
||||||
|
if (pinnedCount == 5 && position > 4) {
|
||||||
|
canPinOneMoreDialog = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val pin = getString(
|
||||||
|
if (conversation.isPinned) R.string.conversation_context_action_unpin
|
||||||
|
else R.string.conversation_context_action_pin
|
||||||
|
)
|
||||||
|
|
||||||
|
val delete = getString(R.string.conversation_context_action_delete)
|
||||||
|
|
||||||
|
val params = mutableListOf<String>()
|
||||||
|
|
||||||
|
if (canPinOneMoreDialog) params += pin
|
||||||
|
|
||||||
|
params += delete
|
||||||
|
|
||||||
|
val arrayParams = params.toTypedArray()
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setItems(arrayParams) { _, which ->
|
||||||
|
when (params[which]) {
|
||||||
|
pin -> showPinConversationDialog(conversation)
|
||||||
|
delete -> showDeleteConversationDialog(conversation.id)
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDeleteConversationDialog(conversationId: Int) {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.confirm_delete_conversation)
|
||||||
|
.setPositiveButton(R.string.action_delete) { _, _ ->
|
||||||
|
viewModel.deleteConversation(conversationId)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteConversation(conversationId: Int) {
|
||||||
|
val index = adapter.removeConversation(conversationId) ?: return
|
||||||
|
adapter.notifyItemRemoved(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPinConversationDialog(conversation: VkConversation) {
|
||||||
|
val isPinned = conversation.isPinned
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(
|
||||||
|
if (isPinned) R.string.confirm_unpin_conversation
|
||||||
|
else R.string.confirm_pin_conversation
|
||||||
|
)
|
||||||
|
.setPositiveButton(
|
||||||
|
if (isPinned) R.string.action_unpin
|
||||||
|
else R.string.action_pin
|
||||||
|
) { _, _ ->
|
||||||
|
viewModel.pinConversation(
|
||||||
|
peerId = conversation.id,
|
||||||
|
pin = !isPinned
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+50
-25
@@ -3,17 +3,14 @@ package com.meloda.fast.screens.conversations
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.meloda.fast.api.UserConfig
|
import com.meloda.fast.api.UserConfig
|
||||||
import com.meloda.fast.api.VKConstants
|
import com.meloda.fast.api.VKConstants
|
||||||
import com.meloda.fast.api.network.datasource.ConversationsDataSource
|
|
||||||
import com.meloda.fast.api.network.datasource.UsersDataSource
|
|
||||||
import com.meloda.fast.api.model.VkConversation
|
import com.meloda.fast.api.model.VkConversation
|
||||||
import com.meloda.fast.api.model.VkGroup
|
import com.meloda.fast.api.model.VkGroup
|
||||||
import com.meloda.fast.api.model.VkUser
|
import com.meloda.fast.api.model.VkUser
|
||||||
import com.meloda.fast.api.model.request.ConversationsGetRequest
|
import com.meloda.fast.api.network.conversations.*
|
||||||
import com.meloda.fast.api.model.request.UsersGetRequest
|
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.BaseViewModel
|
||||||
import com.meloda.fast.base.viewmodel.StartProgressEvent
|
import com.meloda.fast.base.viewmodel.VkEvent
|
||||||
import com.meloda.fast.base.viewmodel.StopProgressEvent
|
|
||||||
import com.meloda.fast.base.viewmodel.VKEvent
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -22,18 +19,20 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ConversationsViewModel @Inject constructor(
|
class ConversationsViewModel @Inject constructor(
|
||||||
private val dataSource: ConversationsDataSource,
|
private val conversations: ConversationsDataSource,
|
||||||
private val usersDataSource: UsersDataSource
|
private val users: UsersDataSource
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
fun loadConversations() = viewModelScope.launch(Dispatchers.Default) {
|
fun loadConversations(
|
||||||
|
offset: Int? = null
|
||||||
|
) = viewModelScope.launch(Dispatchers.Default) {
|
||||||
makeJob({
|
makeJob({
|
||||||
dataSource.getAllChats(
|
conversations.get(
|
||||||
ConversationsGetRequest(
|
ConversationsGetRequest(
|
||||||
count = 30,
|
count = 30,
|
||||||
// offset = 177,
|
|
||||||
extended = true,
|
extended = true,
|
||||||
fields = "${VKConstants.USER_FIELDS},${VKConstants.GROUP_FIELDS}"
|
offset = offset,
|
||||||
|
fields = VKConstants.ALL_FIELDS
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -52,6 +51,7 @@ class ConversationsViewModel @Inject constructor(
|
|||||||
sendEvent(
|
sendEvent(
|
||||||
ConversationsLoaded(
|
ConversationsLoaded(
|
||||||
count = response.count,
|
count = response.count,
|
||||||
|
offset = offset,
|
||||||
unreadCount = response.unreadCount ?: 0,
|
unreadCount = response.unreadCount ?: 0,
|
||||||
conversations = response.items.map { items ->
|
conversations = response.items.map { items ->
|
||||||
items.conversation.asVkConversation(
|
items.conversation.asVkConversation(
|
||||||
@@ -63,34 +63,59 @@ class ConversationsViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
onError = {
|
)
|
||||||
val er = it
|
|
||||||
throw it
|
|
||||||
},
|
|
||||||
onStart = { sendEvent(StartProgressEvent) },
|
|
||||||
onEnd = { sendEvent(StopProgressEvent) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadProfileUser() = viewModelScope.launch {
|
fun loadProfileUser() = viewModelScope.launch {
|
||||||
makeJob({
|
makeJob({ users.getById(UsersGetRequest(fields = VKConstants.USER_FIELDS)) },
|
||||||
usersDataSource.getById(UsersGetRequest(fields = "online,photo_200"))
|
|
||||||
},
|
|
||||||
onAnswer = {
|
onAnswer = {
|
||||||
it.response?.let { r ->
|
it.response?.let { r ->
|
||||||
val users = r.map { u -> u.asVkUser() }
|
val users = r.map { u -> u.asVkUser() }
|
||||||
usersDataSource.storeUsers(users)
|
this@ConversationsViewModel.users.storeUsers(users)
|
||||||
|
|
||||||
UserConfig.vkUser.value = users[0]
|
UserConfig.vkUser.value = users[0]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteConversation(peerId: Int) = viewModelScope.launch {
|
||||||
|
makeJob({
|
||||||
|
conversations.delete(
|
||||||
|
ConversationsDeleteRequest(peerId)
|
||||||
|
)
|
||||||
|
}, onAnswer = { sendEvent(ConversationsDelete(peerId)) })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pinConversation(
|
||||||
|
peerId: Int,
|
||||||
|
pin: Boolean
|
||||||
|
) = viewModelScope.launch {
|
||||||
|
if (pin) {
|
||||||
|
makeJob(
|
||||||
|
{ conversations.pin(ConversationsPinRequest(peerId)) },
|
||||||
|
onAnswer = { sendEvent(ConversationsPin(peerId)) }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
makeJob(
|
||||||
|
{ conversations.unpin(ConversationsUnpinRequest(peerId)) },
|
||||||
|
onAnswer = { sendEvent(ConversationsUnpin(peerId)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ConversationsLoaded(
|
data class ConversationsLoaded(
|
||||||
val count: Int,
|
val count: Int,
|
||||||
|
val offset: Int?,
|
||||||
val unreadCount: Int?,
|
val unreadCount: Int?,
|
||||||
val conversations: List<VkConversation>,
|
val conversations: List<VkConversation>,
|
||||||
val profiles: HashMap<Int, VkUser>,
|
val profiles: HashMap<Int, VkUser>,
|
||||||
val groups: HashMap<Int, VkGroup>
|
val groups: HashMap<Int, VkGroup>
|
||||||
) : VKEvent()
|
) : VkEvent()
|
||||||
|
|
||||||
|
data class ConversationsDelete(val peerId: Int) : VkEvent()
|
||||||
|
|
||||||
|
data class ConversationsPin(val peerId: Int) : VkEvent()
|
||||||
|
|
||||||
|
data class ConversationsUnpin(val peerId: Int) : VkEvent()
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
package com.meloda.fast.screens.login
|
package com.meloda.fast.screens.login
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.viewbinding.library.fragment.viewBinding
|
import android.viewbinding.library.fragment.viewBinding
|
||||||
|
import android.webkit.CookieManager
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
@@ -19,6 +25,8 @@ import com.google.android.material.snackbar.Snackbar
|
|||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.meloda.fast.BuildConfig
|
import com.meloda.fast.BuildConfig
|
||||||
import com.meloda.fast.R
|
import com.meloda.fast.R
|
||||||
|
import com.meloda.fast.api.UserConfig
|
||||||
|
import com.meloda.fast.api.VKConstants
|
||||||
import com.meloda.fast.base.BaseViewModelFragment
|
import com.meloda.fast.base.BaseViewModelFragment
|
||||||
import com.meloda.fast.base.viewmodel.*
|
import com.meloda.fast.base.viewmodel.*
|
||||||
import com.meloda.fast.databinding.DialogCaptchaBinding
|
import com.meloda.fast.databinding.DialogCaptchaBinding
|
||||||
@@ -29,7 +37,10 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import java.net.URLEncoder
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.regex.Pattern
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -59,14 +70,14 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
|||||||
binding.loginInput.clearFocus()
|
binding.loginInput.clearFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEvent(event: VKEvent) {
|
override fun onEvent(event: VkEvent) {
|
||||||
super.onEvent(event)
|
super.onEvent(event)
|
||||||
|
|
||||||
when (event) {
|
when (event) {
|
||||||
is ShowError -> showErrorSnackbar(event.errorDescription)
|
is ErrorEvent -> showErrorSnackbar(event.errorText)
|
||||||
is CaptchaEvent -> showCaptchaDialog(event.sid, event.image)
|
is CaptchaEvent -> showCaptchaDialog(event.sid, event.image)
|
||||||
is ValidationEvent -> showValidationRequired(event.sid)
|
is ValidationEvent -> showValidationRequired(event.sid)
|
||||||
is SuccessAuth -> goToMain(event.haveAuthorized)
|
is SuccessAuth -> goToMain(event)
|
||||||
|
|
||||||
is CodeSent -> showValidationDialog()
|
is CodeSent -> showValidationDialog()
|
||||||
is StartProgressEvent -> onProgressStarted()
|
is StartProgressEvent -> onProgressStarted()
|
||||||
@@ -89,11 +100,91 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun prepareViews() {
|
private fun prepareViews() {
|
||||||
|
prepareWebView()
|
||||||
prepareEmailEditText()
|
prepareEmailEditText()
|
||||||
preparePasswordEditText()
|
preparePasswordEditText()
|
||||||
prepareAuthButton()
|
prepareAuthButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
private fun prepareWebView() {
|
||||||
|
with(binding.webView) {
|
||||||
|
settings.javaScriptEnabled = true
|
||||||
|
settings.domStorageEnabled = true
|
||||||
|
|
||||||
|
clearCache(true)
|
||||||
|
webViewClient = object : WebViewClient() {
|
||||||
|
override fun onPageStarted(view: WebView?, url: String, favicon: Bitmap?) {
|
||||||
|
super.onPageStarted(view, url, favicon)
|
||||||
|
parseAuthUrl(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageFinished(view: WebView, url: String) {
|
||||||
|
super.onPageFinished(view, url)
|
||||||
|
|
||||||
|
val a = Jsoup.parse(url)
|
||||||
|
|
||||||
|
val b = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CookieManager.getInstance().apply {
|
||||||
|
removeAllCookies(null)
|
||||||
|
flush()
|
||||||
|
setAcceptCookie(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun launchWebView() {
|
||||||
|
binding.webView.loadUrl(
|
||||||
|
"https://oauth.vk.com/authorize?client_id=${UserConfig.FAST_APP_ID}&" +
|
||||||
|
"display=mobile&scope=136297695&" +
|
||||||
|
"redirect_uri=${
|
||||||
|
URLEncoder.encode(
|
||||||
|
"https://oauth.vk.com/blank.html",
|
||||||
|
Charsets.UTF_8.toString()
|
||||||
|
)
|
||||||
|
}&response_type=token&v=${VKConstants.API_VERSION}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseAuthUrl(url: String) {
|
||||||
|
if (url.isBlank()) return
|
||||||
|
|
||||||
|
if (url.startsWith("https://oauth.vk.com/blank.html")) {
|
||||||
|
if (url.contains("error")) {
|
||||||
|
Log.e("Fast::Login", "errorUrl: $url")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val authData = parseRedirectUrl(url)
|
||||||
|
if (authData == null) {
|
||||||
|
Log.e("Fast::Login", "errorUrl: $url")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val token = authData.first
|
||||||
|
|
||||||
|
UserConfig.fastToken = token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseRedirectUrl(url: String): Pair<String, Int>? {
|
||||||
|
val accessToken = extractPattern(url, "access_token=(.*?)&") ?: return null
|
||||||
|
val userId = extractPattern(url, "id=(\\d*)")?.toIntOrNull() ?: return null
|
||||||
|
|
||||||
|
return accessToken to userId
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractPattern(string: String, pattern: String): String? {
|
||||||
|
val p = Pattern.compile(pattern)
|
||||||
|
val m = p.matcher(string)
|
||||||
|
return if (m.find()) {
|
||||||
|
m.group(1)
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
private fun prepareEmailEditText() {
|
private fun prepareEmailEditText() {
|
||||||
binding.loginInput.addTextChangedListener {
|
binding.loginInput.addTextChangedListener {
|
||||||
if (!binding.loginLayout.error.isNullOrBlank()) binding.loginLayout.error = ""
|
if (!binding.loginLayout.error.isNullOrBlank()) binding.loginLayout.error = ""
|
||||||
@@ -293,8 +384,13 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
|||||||
snackbar.show()
|
snackbar.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun goToMain(haveAuthorized: Boolean) = lifecycleScope.launch {
|
private fun goToMain(event: SuccessAuth) = lifecycleScope.launch {
|
||||||
if (haveAuthorized) delay(500)
|
UserConfig.userId = event.userId
|
||||||
|
UserConfig.accessToken = event.vkToken
|
||||||
|
|
||||||
|
if (event.haveAuthorized) delay(500)
|
||||||
|
|
||||||
|
launchWebView()
|
||||||
|
|
||||||
findNavController().navigate(R.id.toMain)
|
findNavController().navigate(R.id.toMain)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
package com.meloda.fast.screens.login
|
package com.meloda.fast.screens.login
|
||||||
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.meloda.fast.api.UserConfig
|
|
||||||
import com.meloda.fast.api.VKConstants
|
import com.meloda.fast.api.VKConstants
|
||||||
import com.meloda.fast.api.VKException
|
import com.meloda.fast.api.VKException
|
||||||
import com.meloda.fast.api.model.request.RequestAuthDirect
|
import com.meloda.fast.api.network.auth.AuthDataSource
|
||||||
import com.meloda.fast.api.network.datasource.AuthDataSource
|
import com.meloda.fast.api.network.auth.RequestAuthDirect
|
||||||
import com.meloda.fast.base.viewmodel.*
|
import com.meloda.fast.base.viewmodel.*
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -45,33 +44,37 @@ class LoginViewModel @Inject constructor(
|
|||||||
return@makeJob
|
return@makeJob
|
||||||
}
|
}
|
||||||
|
|
||||||
UserConfig.userId = it.userId
|
sendEvent(
|
||||||
UserConfig.accessToken = it.accessToken
|
SuccessAuth(
|
||||||
|
userId = it.userId,
|
||||||
sendEvent(SuccessAuth())
|
vkToken = it.accessToken
|
||||||
|
)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onError = {
|
onError = {
|
||||||
if (it !is VKException) return@makeJob
|
if (it !is VKException) {
|
||||||
|
onError(it)
|
||||||
|
return@makeJob
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 9/27/2021 use `delay` parameter
|
||||||
twoFaCode?.let { sendEvent(CodeSent) }
|
twoFaCode?.let { sendEvent(CodeSent) }
|
||||||
},
|
}
|
||||||
onStart = { sendEvent(StartProgressEvent) },
|
|
||||||
onEnd = { sendEvent(StopProgressEvent) }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendSms(validationSid: String) = viewModelScope.launch {
|
fun sendSms(validationSid: String) = viewModelScope.launch {
|
||||||
makeJob({ dataSource.sendSms(validationSid) },
|
makeJob({ dataSource.sendSms(validationSid) },
|
||||||
onAnswer = { sendEvent(CodeSent) },
|
onAnswer = { sendEvent(CodeSent) }
|
||||||
onError = {},
|
)
|
||||||
onStart = {},
|
|
||||||
onEnd = {})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ShowError(val errorDescription: String) : VKEvent()
|
object CodeSent : VkEvent()
|
||||||
|
|
||||||
object CodeSent : VKEvent()
|
data class SuccessAuth(
|
||||||
|
val haveAuthorized: Boolean = true,
|
||||||
data class SuccessAuth(val haveAuthorized: Boolean = true) : VKEvent()
|
val userId: Int,
|
||||||
|
val vkToken: String
|
||||||
|
) : VkEvent()
|
||||||
@@ -42,5 +42,4 @@ class MainFragment : BaseViewModelFragment<MainViewModel>(R.layout.fragment_main
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -15,9 +15,11 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.view.isNotEmpty
|
import androidx.core.view.isNotEmpty
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.setPadding
|
import androidx.core.view.setPadding
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
import coil.load
|
import coil.load
|
||||||
import com.google.android.material.imageview.ShapeableImageView
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
import com.meloda.fast.R
|
import com.meloda.fast.R
|
||||||
|
import com.meloda.fast.api.UserConfig
|
||||||
import com.meloda.fast.api.VkUtils
|
import com.meloda.fast.api.VkUtils
|
||||||
import com.meloda.fast.api.model.VkGroup
|
import com.meloda.fast.api.model.VkGroup
|
||||||
import com.meloda.fast.api.model.VkMessage
|
import com.meloda.fast.api.model.VkMessage
|
||||||
@@ -30,9 +32,11 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
// TODO: 9/29/2021 use recyclerview for viewing attachments
|
||||||
class AttachmentInflater constructor(
|
class AttachmentInflater constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val container: LinearLayoutCompat,
|
private val container: LinearLayoutCompat,
|
||||||
|
private val textContainer: LinearLayoutCompat,
|
||||||
private val message: VkMessage,
|
private val message: VkMessage,
|
||||||
private val profiles: Map<Int, VkUser>,
|
private val profiles: Map<Int, VkUser>,
|
||||||
private val groups: Map<Int, VkGroup>
|
private val groups: Map<Int, VkGroup>
|
||||||
@@ -44,16 +48,28 @@ class AttachmentInflater constructor(
|
|||||||
private val playColor = ContextCompat.getColor(context, R.color.a3_700)
|
private val playColor = ContextCompat.getColor(context, R.color.a3_700)
|
||||||
private val playBackgroundColor = ContextCompat.getColor(context, R.color.a3_200)
|
private val playBackgroundColor = ContextCompat.getColor(context, R.color.a3_200)
|
||||||
|
|
||||||
|
var photoClickListener: ((url: String) -> Unit)? = null
|
||||||
|
|
||||||
|
fun setPhotoClickListener(unit: ((url: String) -> Unit)?): AttachmentInflater {
|
||||||
|
this.photoClickListener = unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
fun inflate() {
|
fun inflate() {
|
||||||
if (message.attachments.isNullOrEmpty()) return
|
if (message.attachments.isNullOrEmpty()) return
|
||||||
attachments = message.attachments!!
|
attachments = message.attachments!!
|
||||||
|
|
||||||
container.removeAllViews()
|
container.removeAllViews()
|
||||||
|
textContainer.removeAllViews()
|
||||||
|
|
||||||
if (attachments.size == 1) {
|
if (attachments.size == 1) {
|
||||||
when (val attachment = attachments[0]) {
|
when (val attachment = attachments[0]) {
|
||||||
is VkSticker -> return sticker(attachment)
|
is VkSticker -> return sticker(attachment)
|
||||||
is VkWall -> return wall(attachment)
|
is VkWall -> return wall(attachment)
|
||||||
|
is VkVoiceMessage -> return voice(attachment)
|
||||||
|
is VkCall -> return call(attachment)
|
||||||
|
is VkGraffiti -> return graffiti(attachment)
|
||||||
|
is VkGift -> return gift(attachment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +98,6 @@ class AttachmentInflater constructor(
|
|||||||
is VkAudio -> audio(attachment)
|
is VkAudio -> audio(attachment)
|
||||||
is VkFile -> file(attachment)
|
is VkFile -> file(attachment)
|
||||||
is VkLink -> link(attachment)
|
is VkLink -> link(attachment)
|
||||||
is VkStory -> story(attachment)
|
|
||||||
|
|
||||||
else -> Log.e(
|
else -> Log.e(
|
||||||
"Attachment inflater",
|
"Attachment inflater",
|
||||||
@@ -94,12 +109,15 @@ class AttachmentInflater constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun photo(photo: VkPhoto) {
|
private fun photo(photo: VkPhoto) {
|
||||||
val size = photo.sizeOfType('m') ?: return
|
val size = photo.getSizeOrSmaller('y') ?: return
|
||||||
|
|
||||||
val newPhoto = ShapeableImageView(context).apply {
|
val newPhoto = ShapeableImageView(context).apply {
|
||||||
layoutParams = LinearLayoutCompat.LayoutParams(
|
layoutParams = LinearLayoutCompat.LayoutParams(
|
||||||
AndroidUtils.px(size.width).roundToInt(),
|
// ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
AndroidUtils.px(size.height).roundToInt()
|
size.width,
|
||||||
|
size.height
|
||||||
|
// AndroidUtils.px(size.width).roundToInt(),
|
||||||
|
// AndroidUtils.px(size.height).roundToInt()
|
||||||
)
|
)
|
||||||
|
|
||||||
shapeAppearanceModel =
|
shapeAppearanceModel =
|
||||||
@@ -110,6 +128,12 @@ class AttachmentInflater constructor(
|
|||||||
scaleType = ImageView.ScaleType.CENTER_CROP
|
scaleType = ImageView.ScaleType.CENTER_CROP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (photoClickListener != null) {
|
||||||
|
newPhoto.setOnClickListener { photoClickListener?.invoke(size.url) }
|
||||||
|
} else {
|
||||||
|
newPhoto.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
|
||||||
val spacer = Space(context).also {
|
val spacer = Space(context).also {
|
||||||
it.layoutParams = LinearLayoutCompat.LayoutParams(
|
it.layoutParams = LinearLayoutCompat.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
@@ -222,14 +246,7 @@ class AttachmentInflater constructor(
|
|||||||
binding.caption.text = link.caption
|
binding.caption.text = link.caption
|
||||||
binding.caption.isVisible = !link.caption.isNullOrBlank()
|
binding.caption.isVisible = !link.caption.isNullOrBlank()
|
||||||
|
|
||||||
binding.preview.shapeAppearanceModel.toBuilder()
|
link.photo?.getMaxSize()?.let {
|
||||||
.setAllCornerSizes(40f)
|
|
||||||
.build()
|
|
||||||
.let {
|
|
||||||
binding.preview.shapeAppearanceModel = it
|
|
||||||
}
|
|
||||||
|
|
||||||
link.photo?.sizeOfType('m')?.let {
|
|
||||||
binding.preview.load(it.url) { crossfade(150) }
|
binding.preview.load(it.url) { crossfade(150) }
|
||||||
binding.preview.isVisible = true
|
binding.preview.isVisible = true
|
||||||
return
|
return
|
||||||
@@ -245,8 +262,8 @@ class AttachmentInflater constructor(
|
|||||||
|
|
||||||
with(binding.image) {
|
with(binding.image) {
|
||||||
layoutParams = LinearLayoutCompat.LayoutParams(
|
layoutParams = LinearLayoutCompat.LayoutParams(
|
||||||
AndroidUtils.px(180).roundToInt(),
|
AndroidUtils.px(140).roundToInt(),
|
||||||
AndroidUtils.px(180).roundToInt()
|
AndroidUtils.px(140).roundToInt()
|
||||||
)
|
)
|
||||||
|
|
||||||
load(url) { crossfade(150) }
|
load(url) { crossfade(150) }
|
||||||
@@ -282,7 +299,7 @@ class AttachmentInflater constructor(
|
|||||||
|
|
||||||
binding.avatar.isVisible = group != null || user != null
|
binding.avatar.isVisible = group != null || user != null
|
||||||
binding.avatar.shapeAppearanceModel.toBuilder()
|
binding.avatar.shapeAppearanceModel.toBuilder()
|
||||||
.setAllCornerSizes(40f)
|
.setAllCornerSizes(AndroidUtils.px(20))
|
||||||
.build()
|
.build()
|
||||||
.let {
|
.let {
|
||||||
binding.avatar.shapeAppearanceModel = it
|
binding.avatar.shapeAppearanceModel = it
|
||||||
@@ -302,8 +319,91 @@ class AttachmentInflater constructor(
|
|||||||
).format(wall.date * 1000L)
|
).format(wall.date * 1000L)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun story(story: VkStory) {
|
private fun voice(voiceMessage: VkVoiceMessage) {
|
||||||
|
val binding = ItemMessageAttachmentVoiceBinding.inflate(inflater, textContainer, true)
|
||||||
|
|
||||||
|
if (message.isOut)
|
||||||
|
binding.root.updatePadding(
|
||||||
|
bottom = AndroidUtils.px(6).roundToInt(),
|
||||||
|
left = AndroidUtils.px(6).roundToInt()
|
||||||
|
)
|
||||||
|
|
||||||
|
val waveform = IntArray(voiceMessage.waveform.size)
|
||||||
|
voiceMessage.waveform.forEachIndexed { index, i -> waveform[index] = i }
|
||||||
|
|
||||||
|
binding.waveform.sample = waveform
|
||||||
|
binding.waveform.maxProgress = 100f
|
||||||
|
binding.waveform.progress = 100f
|
||||||
|
|
||||||
|
binding.duration.text = SimpleDateFormat(
|
||||||
|
"mm:ss",
|
||||||
|
Locale.getDefault()
|
||||||
|
).format(voiceMessage.duration * 1000L)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun call(call: VkCall) {
|
||||||
|
val binding = ItemMessageAttachmentCallBinding.inflate(inflater, textContainer, true)
|
||||||
|
|
||||||
|
if (message.isOut)
|
||||||
|
binding.root.updatePadding(
|
||||||
|
bottom = AndroidUtils.px(5).roundToInt(),
|
||||||
|
left = AndroidUtils.px(6).roundToInt()
|
||||||
|
)
|
||||||
|
|
||||||
|
val callType =
|
||||||
|
context.getString(
|
||||||
|
if (call.initiatorId == UserConfig.userId) R.string.message_call_type_outgoing
|
||||||
|
else R.string.message_call_type_incoming
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.type.text = callType
|
||||||
|
|
||||||
|
var callState =
|
||||||
|
context.getString(
|
||||||
|
if (call.state == "reached") R.string.message_call_state_ended
|
||||||
|
else if (call.state == "canceled_by_initiator") {
|
||||||
|
if (call.initiatorId == UserConfig.userId) R.string.message_call_state_cancelled
|
||||||
|
else R.string.message_call_state_missed
|
||||||
|
} else R.string.message_call_unknown
|
||||||
|
)
|
||||||
|
|
||||||
|
if (callState == context.getString(R.string.message_call_unknown)) callState = call.state
|
||||||
|
|
||||||
|
binding.state.text = callState
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun graffiti(graffiti: VkGraffiti) {
|
||||||
|
val binding = ItemMessageAttachmentGraffitiBinding.inflate(inflater, container, true)
|
||||||
|
|
||||||
|
val url = graffiti.url
|
||||||
|
|
||||||
|
val heightCoefficient = graffiti.height / AndroidUtils.px(140)
|
||||||
|
|
||||||
|
with(binding.image) {
|
||||||
|
layoutParams = LinearLayoutCompat.LayoutParams(
|
||||||
|
AndroidUtils.px(140).roundToInt(),
|
||||||
|
(graffiti.height / heightCoefficient).roundToInt()
|
||||||
|
)
|
||||||
|
|
||||||
|
load(url) { crossfade(150) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun gift(gift: VkGift) {
|
||||||
|
val binding = ItemMessageAttachmentGiftBinding.inflate(inflater, container, true)
|
||||||
|
|
||||||
|
val url = gift.thumb256 ?: gift.thumb96 ?: gift.thumb48
|
||||||
|
|
||||||
|
with(binding.image) {
|
||||||
|
shapeAppearanceModel = shapeAppearanceModel.withCornerSize { AndroidUtils.px(12) }
|
||||||
|
|
||||||
|
layoutParams = LinearLayoutCompat.LayoutParams(
|
||||||
|
AndroidUtils.px(140).roundToInt(),
|
||||||
|
AndroidUtils.px(140).roundToInt()
|
||||||
|
)
|
||||||
|
|
||||||
|
load(url) { crossfade(150) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import android.graphics.Color
|
|||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.appcompat.widget.LinearLayoutCompat
|
import androidx.appcompat.widget.LinearLayoutCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
@@ -29,7 +30,9 @@ class MessagesHistoryAdapter constructor(
|
|||||||
val conversation: VkConversation,
|
val conversation: VkConversation,
|
||||||
val profiles: HashMap<Int, VkUser> = hashMapOf(),
|
val profiles: HashMap<Int, VkUser> = hashMapOf(),
|
||||||
val groups: HashMap<Int, VkGroup> = hashMapOf()
|
val groups: HashMap<Int, VkGroup> = hashMapOf()
|
||||||
) : BaseAdapter<VkMessage, MessagesHistoryAdapter.Holder>(context, values, COMPARATOR) {
|
) : BaseAdapter<VkMessage, MessagesHistoryAdapter.BasicHolder>(context, values, COMPARATOR) {
|
||||||
|
|
||||||
|
var avatarLongClickListener: ((position: Int) -> Unit)? = null
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
when {
|
when {
|
||||||
@@ -49,7 +52,7 @@ class MessagesHistoryAdapter constructor(
|
|||||||
private fun isPositionHeader(position: Int) = position == 0
|
private fun isPositionHeader(position: Int) = position == 0
|
||||||
private fun isPositionFooter(position: Int) = position >= actualSize
|
private fun isPositionFooter(position: Int) = position >= actualSize
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BasicHolder {
|
||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
// magick numbers is great!
|
// magick numbers is great!
|
||||||
HEADER -> Header(createEmptyView(60))
|
HEADER -> Header(createEmptyView(60))
|
||||||
@@ -67,6 +70,21 @@ class MessagesHistoryAdapter constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// override fun initListeners(itemView: View, position: Int) {
|
||||||
|
// if (itemView is AdapterView<*>) return
|
||||||
|
//
|
||||||
|
// itemView.setOnClickListener { onItemClickListener?.invoke(position, itemView) }
|
||||||
|
// itemView.setOnLongClickListener { itemLongClickListener.invoke(position) }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
val actualSize get() = values.size
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
if (actualSize == 0) return 2
|
||||||
|
return super.getItemCount() + 2
|
||||||
|
}
|
||||||
|
|
||||||
private fun createEmptyView(size: Int) = View(context).apply {
|
private fun createEmptyView(size: Int) = View(context).apply {
|
||||||
layoutParams = ViewGroup.LayoutParams(
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
@@ -78,22 +96,22 @@ class MessagesHistoryAdapter constructor(
|
|||||||
isFocusable = false
|
isFocusable = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: Holder, position: Int) {
|
override fun onBindViewHolder(holder: BasicHolder, position: Int) {
|
||||||
if (holder is Header || holder is Footer) return
|
if (holder is Header || holder is Footer) return
|
||||||
|
|
||||||
initListeners(holder.itemView, position)
|
initListeners(holder.itemView, position)
|
||||||
holder.bind(position)
|
holder.bind(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
open inner class Holder(v: View = View(context)) : BaseHolder(v)
|
open inner class BasicHolder(v: View = View(context)) : BaseHolder(v)
|
||||||
|
|
||||||
inner class Header(v: View) : Holder(v)
|
inner class Header(v: View) : BasicHolder(v)
|
||||||
|
|
||||||
inner class Footer(v: View) : Holder(v)
|
inner class Footer(v: View) : BasicHolder(v)
|
||||||
|
|
||||||
inner class IncomingMessage(
|
inner class IncomingMessage(
|
||||||
private val binding: ItemMessageInBinding
|
private val binding: ItemMessageInBinding
|
||||||
) : Holder(binding.root) {
|
) : BasicHolder(binding.root) {
|
||||||
|
|
||||||
override fun bind(position: Int) {
|
override fun bind(position: Int) {
|
||||||
val message = getItem(position)
|
val message = getItem(position)
|
||||||
@@ -103,33 +121,42 @@ class MessagesHistoryAdapter constructor(
|
|||||||
|
|
||||||
MessagesPreparator(
|
MessagesPreparator(
|
||||||
context = context,
|
context = context,
|
||||||
|
|
||||||
|
root = binding.root,
|
||||||
|
|
||||||
conversation = conversation,
|
conversation = conversation,
|
||||||
message = message,
|
message = message,
|
||||||
prevMessage = prevMessage,
|
prevMessage = prevMessage,
|
||||||
nextMessage = nextMessage,
|
nextMessage = nextMessage,
|
||||||
|
|
||||||
|
title = binding.title,
|
||||||
|
|
||||||
avatar = binding.avatar,
|
avatar = binding.avatar,
|
||||||
bubble = binding.bubble,
|
bubble = binding.bubble,
|
||||||
text = binding.text,
|
text = binding.text,
|
||||||
spacer = binding.spacer,
|
spacer = binding.spacer,
|
||||||
time = binding.time,
|
|
||||||
unread = binding.unread,
|
unread = binding.unread,
|
||||||
|
|
||||||
|
textContainer = binding.textContainer,
|
||||||
attachmentContainer = binding.attachmentContainer,
|
attachmentContainer = binding.attachmentContainer,
|
||||||
attachmentSpacer = binding.attachmentSpacer,
|
attachmentSpacer = binding.attachmentSpacer,
|
||||||
|
|
||||||
profiles = profiles,
|
profiles = profiles,
|
||||||
groups = groups
|
groups = groups
|
||||||
).prepare()
|
).setPhotoClickListener {
|
||||||
|
Toast.makeText(context, "Photo url: $it", Toast.LENGTH_LONG).show()
|
||||||
|
}.prepare()
|
||||||
|
|
||||||
|
binding.avatar.setOnLongClickListener() {
|
||||||
|
avatarLongClickListener?.invoke(position)
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class OutgoingMessage(
|
inner class OutgoingMessage(
|
||||||
private val binding: ItemMessageOutBinding
|
private val binding: ItemMessageOutBinding
|
||||||
) : Holder(binding.root) {
|
) : BasicHolder(binding.root) {
|
||||||
|
|
||||||
init {
|
|
||||||
binding.bubbleStroke.setOnClickListener { binding.bubble.performClick() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bind(position: Int) {
|
override fun bind(position: Int) {
|
||||||
val message = getItem(position)
|
val message = getItem(position)
|
||||||
@@ -138,16 +165,17 @@ class MessagesHistoryAdapter constructor(
|
|||||||
|
|
||||||
MessagesPreparator(
|
MessagesPreparator(
|
||||||
context = context,
|
context = context,
|
||||||
|
root = binding.root,
|
||||||
conversation = conversation,
|
conversation = conversation,
|
||||||
message = message,
|
message = message,
|
||||||
prevMessage = prevMessage,
|
prevMessage = prevMessage,
|
||||||
|
|
||||||
bubble = binding.bubble,
|
bubble = binding.bubble,
|
||||||
bubbleStroke = binding.bubbleStroke,
|
|
||||||
text = binding.text,
|
text = binding.text,
|
||||||
spacer = binding.spacer,
|
spacer = binding.spacer,
|
||||||
time = binding.time,
|
|
||||||
unread = binding.unread,
|
unread = binding.unread,
|
||||||
|
|
||||||
|
textContainer = binding.textContainer,
|
||||||
attachmentContainer = binding.attachmentContainer,
|
attachmentContainer = binding.attachmentContainer,
|
||||||
attachmentSpacer = binding.attachmentSpacer,
|
attachmentSpacer = binding.attachmentSpacer,
|
||||||
|
|
||||||
@@ -159,7 +187,7 @@ class MessagesHistoryAdapter constructor(
|
|||||||
|
|
||||||
inner class ServiceMessage(
|
inner class ServiceMessage(
|
||||||
private val binding: ItemMessageServiceBinding
|
private val binding: ItemMessageServiceBinding
|
||||||
) : Holder(binding.root) {
|
) : BasicHolder(binding.root) {
|
||||||
|
|
||||||
private val youPrefix = context.getString(R.string.you_message_prefix)
|
private val youPrefix = context.getString(R.string.you_message_prefix)
|
||||||
|
|
||||||
@@ -198,7 +226,7 @@ class MessagesHistoryAdapter constructor(
|
|||||||
|
|
||||||
binding.photo.isVisible = true
|
binding.photo.isVisible = true
|
||||||
|
|
||||||
val size = attachment.sizeOfType('m') ?: return@let
|
val size = attachment.getSizeOrSmaller('y') ?: return@let
|
||||||
|
|
||||||
binding.photo.layoutParams = LinearLayoutCompat.LayoutParams(
|
binding.photo.layoutParams = LinearLayoutCompat.LayoutParams(
|
||||||
size.width,
|
size.width,
|
||||||
@@ -213,11 +241,39 @@ class MessagesHistoryAdapter constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val actualSize get() = values.size
|
fun removeMessageById(id: Int): Int? {
|
||||||
|
for (i in values.indices) {
|
||||||
|
val message = values[i]
|
||||||
|
if (message.id == id) {
|
||||||
|
values.removeAt(i)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
return null
|
||||||
if (actualSize == 0) return 2
|
}
|
||||||
return super.getItemCount() + 2
|
|
||||||
|
fun removeMessagesByIds(ids: List<Int>): List<Int> {
|
||||||
|
val positions = mutableListOf<Int>()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package com.meloda.fast.screens.messages
|
package com.meloda.fast.screens.messages
|
||||||
|
|
||||||
import android.graphics.Color
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.viewbinding.library.fragment.viewBinding
|
import android.viewbinding.library.fragment.viewBinding
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.setPadding
|
||||||
import androidx.core.widget.doAfterTextChanged
|
import androidx.core.widget.doAfterTextChanged
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
@@ -16,6 +20,8 @@ import coil.load
|
|||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.meloda.fast.R
|
import com.meloda.fast.R
|
||||||
import com.meloda.fast.api.UserConfig
|
import com.meloda.fast.api.UserConfig
|
||||||
|
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.VkConversation
|
||||||
import com.meloda.fast.api.model.VkGroup
|
import com.meloda.fast.api.model.VkGroup
|
||||||
import com.meloda.fast.api.model.VkMessage
|
import com.meloda.fast.api.model.VkMessage
|
||||||
@@ -23,7 +29,8 @@ import com.meloda.fast.api.model.VkUser
|
|||||||
import com.meloda.fast.base.BaseViewModelFragment
|
import com.meloda.fast.base.BaseViewModelFragment
|
||||||
import com.meloda.fast.base.viewmodel.StartProgressEvent
|
import com.meloda.fast.base.viewmodel.StartProgressEvent
|
||||||
import com.meloda.fast.base.viewmodel.StopProgressEvent
|
import com.meloda.fast.base.viewmodel.StopProgressEvent
|
||||||
import com.meloda.fast.base.viewmodel.VKEvent
|
import com.meloda.fast.base.viewmodel.VkEvent
|
||||||
|
import com.meloda.fast.databinding.DialogMessageDeleteBinding
|
||||||
import com.meloda.fast.databinding.FragmentMessagesHistoryBinding
|
import com.meloda.fast.databinding.FragmentMessagesHistoryBinding
|
||||||
import com.meloda.fast.extensions.TextViewExtensions.clear
|
import com.meloda.fast.extensions.TextViewExtensions.clear
|
||||||
import com.meloda.fast.util.AndroidUtils
|
import com.meloda.fast.util.AndroidUtils
|
||||||
@@ -32,6 +39,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MessagesHistoryFragment :
|
class MessagesHistoryFragment :
|
||||||
@@ -43,7 +51,7 @@ class MessagesHistoryFragment :
|
|||||||
private val action = MutableLiveData<Action>()
|
private val action = MutableLiveData<Action>()
|
||||||
|
|
||||||
private enum class Action {
|
private enum class Action {
|
||||||
RECORD, SEND
|
RECORD, SEND, EDIT, DELETE
|
||||||
}
|
}
|
||||||
|
|
||||||
private val user: VkUser? by lazy {
|
private val user: VkUser? by lazy {
|
||||||
@@ -62,14 +70,19 @@ class MessagesHistoryFragment :
|
|||||||
MessagesHistoryAdapter(requireContext(), mutableListOf(), conversation).also {
|
MessagesHistoryAdapter(requireContext(), mutableListOf(), conversation).also {
|
||||||
it.itemClickListener = this::onItemClick
|
it.itemClickListener = this::onItemClick
|
||||||
it.itemLongClickListener = this::onItemLongClick
|
it.itemLongClickListener = this::onItemLongClick
|
||||||
|
it.avatarLongClickListener = this::onAvatarLongClickListener
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var timestampTimer: Timer? = null
|
private var timestampTimer: Timer? = null
|
||||||
|
|
||||||
|
private lateinit var attachmentController: AttachmentPanelController
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
attachmentController = AttachmentPanelController().init()
|
||||||
|
|
||||||
val title = when {
|
val title = when {
|
||||||
conversation.isChat() -> conversation.title
|
conversation.isChat() -> conversation.title
|
||||||
conversation.isUser() -> user?.toString()
|
conversation.isUser() -> user?.toString()
|
||||||
@@ -98,19 +111,7 @@ class MessagesHistoryFragment :
|
|||||||
|
|
||||||
binding.status.text = status ?: "..."
|
binding.status.text = status ?: "..."
|
||||||
|
|
||||||
val avatar = when {
|
prepareAvatar()
|
||||||
conversation.isChat() -> conversation.photo200
|
|
||||||
conversation.isUser() -> user?.photo200
|
|
||||||
conversation.isGroup() -> group?.photo200
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.avatar.load(avatar) {
|
|
||||||
crossfade(false)
|
|
||||||
error(ColorDrawable(Color.RED))
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.online.isVisible = user?.online == true
|
|
||||||
|
|
||||||
prepareViews()
|
prepareViews()
|
||||||
|
|
||||||
@@ -166,8 +167,14 @@ class MessagesHistoryFragment :
|
|||||||
})
|
})
|
||||||
|
|
||||||
binding.message.doAfterTextChanged {
|
binding.message.doAfterTextChanged {
|
||||||
val newValue = if (it.toString().isNotBlank()) Action.SEND
|
val canSend = it.toString().isNotBlank()
|
||||||
else Action.RECORD
|
|
||||||
|
val newValue: Action =
|
||||||
|
when {
|
||||||
|
attachmentController.isEditing -> if (it.isNullOrBlank()) Action.DELETE else Action.EDIT
|
||||||
|
canSend -> Action.SEND
|
||||||
|
else -> Action.RECORD
|
||||||
|
}
|
||||||
|
|
||||||
if (action.value != newValue) action.value = newValue
|
if (action.value != newValue) action.value = newValue
|
||||||
}
|
}
|
||||||
@@ -192,52 +199,159 @@ class MessagesHistoryFragment :
|
|||||||
Action.SEND -> {
|
Action.SEND -> {
|
||||||
binding.action.setImageResource(R.drawable.ic_round_send_24)
|
binding.action.setImageResource(R.drawable.ic_round_send_24)
|
||||||
}
|
}
|
||||||
|
Action.EDIT -> {
|
||||||
|
binding.action.setImageResource(R.drawable.ic_round_done_24)
|
||||||
|
}
|
||||||
|
Action.DELETE -> {
|
||||||
|
binding.action.setImageResource(R.drawable.ic_trash_can_outline_24)
|
||||||
|
}
|
||||||
else -> return@observe
|
else -> return@observe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attachmentController.isPanelVisible.observe(viewLifecycleOwner) {
|
||||||
|
if (it) 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.attachmentPanel.setOnClickListener c@{
|
||||||
|
val message = attachmentController.message.value ?: return@c
|
||||||
|
|
||||||
|
val index = adapter.values.indexOf(message)
|
||||||
|
if (index == -1) return@c
|
||||||
|
|
||||||
|
binding.recyclerView.smoothScrollToPosition(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.dismissReply.setOnClickListener {
|
||||||
|
if (attachmentController.message.value != null)
|
||||||
|
attachmentController.message.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prepareAvatar() {
|
||||||
|
val avatar = when {
|
||||||
|
conversation.ownerId == VKConstants.FAST_GROUP_ID -> null
|
||||||
|
conversation.isUser() -> user?.photo200
|
||||||
|
conversation.isGroup() -> group?.photo200
|
||||||
|
conversation.isChat() -> conversation.photo200
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.avatar.isVisible = avatar != null
|
||||||
|
|
||||||
|
if (avatar == null) {
|
||||||
|
binding.avatarPlaceholder.isVisible = true
|
||||||
|
|
||||||
|
if (conversation.ownerId == VKConstants.FAST_GROUP_ID) {
|
||||||
|
binding.placeholderBack.setImageDrawable(
|
||||||
|
ColorDrawable(
|
||||||
|
ContextCompat.getColor(requireContext(), R.color.a1_400)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
binding.placeholder.imageTintList =
|
||||||
|
ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.a1_0))
|
||||||
|
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.placeholder.imageTintList =
|
||||||
|
ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.n2_500))
|
||||||
|
binding.placeholder.setImageResource(R.drawable.ic_account_circle_cut)
|
||||||
|
binding.placeholder.setPadding(0)
|
||||||
|
binding.avatar.setImageDrawable(null)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.avatar.load(avatar) {
|
||||||
|
crossfade(200)
|
||||||
|
target {
|
||||||
|
binding.avatarPlaceholder.isVisible = false
|
||||||
|
binding.avatar.setImageDrawable(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.phantomIcon.isVisible = conversation.isPhantom
|
||||||
|
binding.online.isVisible = user?.online == true
|
||||||
|
binding.pin.isVisible = conversation.isPinned
|
||||||
|
}
|
||||||
|
|
||||||
private fun performAction() {
|
private fun performAction() {
|
||||||
if (action.value == Action.RECORD) {
|
when (action.value) {
|
||||||
|
Action.RECORD -> {
|
||||||
} else if (action.value == Action.SEND) {
|
}
|
||||||
|
Action.SEND -> {
|
||||||
val messageText = binding.message.text.toString().trim()
|
val messageText = binding.message.text.toString().trim()
|
||||||
if (messageText.isBlank()) return
|
if (messageText.isBlank()) return
|
||||||
|
|
||||||
val date = System.currentTimeMillis()
|
val date = System.currentTimeMillis()
|
||||||
|
|
||||||
var message = VkMessage(
|
val message = VkMessage(
|
||||||
id = -1,
|
id = -1,
|
||||||
text = messageText,
|
text = messageText,
|
||||||
isOut = true,
|
isOut = true,
|
||||||
peerId = conversation.id,
|
peerId = conversation.id,
|
||||||
fromId = UserConfig.userId,
|
fromId = UserConfig.userId,
|
||||||
date = (date / 1000).toInt(),
|
date = (date / 1000).toInt(),
|
||||||
randomId = 0
|
randomId = 0,
|
||||||
|
replyMessage = attachmentController.message.value
|
||||||
)
|
)
|
||||||
|
|
||||||
adapter.add(message)
|
adapter.add(message)
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyItemInserted(adapter.actualSize - 1)
|
||||||
binding.recyclerView.smoothScrollToPosition(adapter.lastPosition)
|
binding.recyclerView.smoothScrollToPosition(adapter.lastPosition)
|
||||||
binding.message.clear()
|
binding.message.clear()
|
||||||
|
|
||||||
|
val replyMessage = attachmentController.message.value
|
||||||
|
attachmentController.message.value = null
|
||||||
|
|
||||||
viewModel.sendMessage(
|
viewModel.sendMessage(
|
||||||
peerId = conversation.id,
|
peerId = conversation.id,
|
||||||
message = messageText,
|
message = messageText,
|
||||||
randomId = 0
|
randomId = 0,
|
||||||
) { message = message.copyMessage(id = it) }
|
replyTo = replyMessage?.id
|
||||||
|
) { message.id = it }
|
||||||
|
}
|
||||||
|
Action.EDIT -> {
|
||||||
|
val message = attachmentController.message.value ?: return
|
||||||
|
val messageText = binding.message.text.toString().trim()
|
||||||
|
|
||||||
|
attachmentController.message.value = null
|
||||||
|
|
||||||
|
viewModel.editMessage(
|
||||||
|
originalMessage = message,
|
||||||
|
peerId = conversation.id,
|
||||||
|
messageId = message.id,
|
||||||
|
message = messageText,
|
||||||
|
attachments = message.attachments
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Action.DELETE -> attachmentController.message.value?.let {
|
||||||
|
showDeleteMessageDialog(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEvent(event: VKEvent) {
|
override fun onEvent(event: VkEvent) {
|
||||||
super.onEvent(event)
|
super.onEvent(event)
|
||||||
|
|
||||||
when (event) {
|
when (event) {
|
||||||
is MessagesMarkAsImportant -> markMessagesAsImportant(event)
|
|
||||||
is MessagesLoaded -> refreshMessages(event)
|
|
||||||
is StartProgressEvent -> onProgressStarted()
|
is StartProgressEvent -> onProgressStarted()
|
||||||
is StopProgressEvent -> onProgressStopped()
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,17 +397,21 @@ class MessagesHistoryFragment :
|
|||||||
|
|
||||||
private fun markMessagesAsImportant(event: MessagesMarkAsImportant) {
|
private fun markMessagesAsImportant(event: MessagesMarkAsImportant) {
|
||||||
var changed = false
|
var changed = false
|
||||||
|
val positions = mutableListOf<Int>()
|
||||||
|
|
||||||
for (i in adapter.values.indices) {
|
for (i in adapter.values.indices) {
|
||||||
val message = adapter.values[i]
|
val message = adapter.values[i]
|
||||||
|
message.important = event.important
|
||||||
if (event.messagesIds.contains(message.id)) {
|
if (event.messagesIds.contains(message.id)) {
|
||||||
if (!changed) changed = true
|
if (!changed) changed = true
|
||||||
adapter.values[i] = message.copyMessage(
|
|
||||||
important = event.important
|
positions.add(i)
|
||||||
)
|
|
||||||
|
adapter.values[i] = message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changed) adapter.notifyDataSetChanged()
|
if (changed) positions.forEach { adapter.notifyItemChanged(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshMessages(event: MessagesLoaded) {
|
private fun refreshMessages(event: MessagesLoaded) {
|
||||||
@@ -315,30 +433,235 @@ class MessagesHistoryFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onItemClick(position: Int) {
|
private fun onItemClick(position: Int) {
|
||||||
|
showOptionsDialog(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onItemLongClick(position: Int) = true
|
||||||
|
|
||||||
|
private fun onAvatarLongClickListener(position: Int) {
|
||||||
|
val message = adapter.values[position]
|
||||||
|
|
||||||
|
val messageUser = VkUtils.getMessageUser(message, adapter.profiles)
|
||||||
|
val messageGroup = VkUtils.getMessageGroup(message, adapter.groups)
|
||||||
|
|
||||||
|
val title = VkUtils.getMessageTitle(message, messageUser, messageGroup)
|
||||||
|
Toast.makeText(requireContext(), title, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showOptionsDialog(position: Int) {
|
||||||
val message = adapter.values[position]
|
val message = adapter.values[position]
|
||||||
if (message.action != null) return
|
if (message.action != null) return
|
||||||
|
|
||||||
val important = if (message.important) "Unmark as important" else "Mark as important"
|
val time = getString(
|
||||||
|
R.string.time_format,
|
||||||
|
SimpleDateFormat(
|
||||||
|
"dd.MM.yyyy, HH:mm:ss",
|
||||||
|
Locale.getDefault()
|
||||||
|
).format(message.date * 1000L)
|
||||||
|
)
|
||||||
|
|
||||||
val params = arrayOf(important)
|
val important = getString(
|
||||||
|
if (message.important) R.string.message_context_action_unmark_as_important
|
||||||
|
else R.string.message_context_action_mark_as_important
|
||||||
|
)
|
||||||
|
|
||||||
val dialog = MaterialAlertDialogBuilder(requireContext())
|
val reply = getString(R.string.message_context_action_reply)
|
||||||
.setItems(params) { _, which ->
|
|
||||||
if (which == 0) {
|
val isMessageAlreadyPinned = message.id == conversation.pinnedMessage?.id
|
||||||
viewModel.markAsImportant(
|
|
||||||
|
val pin = getString(
|
||||||
|
if (isMessageAlreadyPinned) R.string.message_context_action_unpin
|
||||||
|
else R.string.message_context_action_pin
|
||||||
|
)
|
||||||
|
|
||||||
|
val edit = getString(R.string.message_context_action_edit)
|
||||||
|
|
||||||
|
val delete = getString(R.string.message_context_action_delete)
|
||||||
|
|
||||||
|
val params = mutableListOf(
|
||||||
|
important, reply
|
||||||
|
)
|
||||||
|
|
||||||
|
if (conversation.canChangePin) {
|
||||||
|
params += pin
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.canEdit()) {
|
||||||
|
params += edit
|
||||||
|
}
|
||||||
|
|
||||||
|
params += delete
|
||||||
|
|
||||||
|
val arrayParams = params.toTypedArray()
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(time)
|
||||||
|
.setItems(arrayParams) { _, which ->
|
||||||
|
when (params[which]) {
|
||||||
|
important -> viewModel.markAsImportant(
|
||||||
messagesIds = listOf(message.id),
|
messagesIds = listOf(message.id),
|
||||||
important = !message.important
|
important = !message.important
|
||||||
)
|
)
|
||||||
|
reply -> {
|
||||||
|
if (attachmentController.message.value != message)
|
||||||
|
attachmentController.message.value = message
|
||||||
|
}
|
||||||
|
pin ->
|
||||||
|
showPinMessageDialog(
|
||||||
|
peerId = conversation.id,
|
||||||
|
messageId = message.id,
|
||||||
|
pin = !isMessageAlreadyPinned
|
||||||
|
)
|
||||||
|
edit -> {
|
||||||
|
attachmentController.isEditing = true
|
||||||
|
|
||||||
|
if (attachmentController.message.value != message)
|
||||||
|
attachmentController.message.value = message
|
||||||
|
}
|
||||||
|
delete -> showDeleteMessageDialog(message)
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPinMessageDialog(
|
||||||
|
peerId: Int,
|
||||||
|
messageId: Int?,
|
||||||
|
pin: Boolean
|
||||||
|
) {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(
|
||||||
|
if (pin) R.string.confirm_pin_message
|
||||||
|
else R.string.confirm_unpin_message
|
||||||
|
)
|
||||||
|
.setPositiveButton(
|
||||||
|
if (pin) R.string.action_pin
|
||||||
|
else R.string.action_unpin
|
||||||
|
) { _, _ ->
|
||||||
|
viewModel.pinMessage(
|
||||||
|
peerId = peerId,
|
||||||
|
messageId = messageId,
|
||||||
|
pin = pin
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDeleteMessageDialog(message: VkMessage) {
|
||||||
|
val binding = DialogMessageDeleteBinding.inflate(layoutInflater, null, false)
|
||||||
|
|
||||||
|
binding.check.setText(
|
||||||
|
if (message.isOut) R.string.message_delete_for_all
|
||||||
|
else R.string.message_mark_as_spam
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.check.isEnabled =
|
||||||
|
(conversation.id != UserConfig.userId) && (!message.isOut || message.canEdit())
|
||||||
|
|
||||||
|
if (conversation.id == UserConfig.userId) binding.check.isChecked = true
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.confirm_delete_message)
|
||||||
|
.setView(binding.root)
|
||||||
|
.setPositiveButton(R.string.action_delete) { _, _ ->
|
||||||
|
attachmentController.message.value = null
|
||||||
|
|
||||||
|
viewModel.deleteMessage(
|
||||||
|
peerId = conversation.id,
|
||||||
|
messagesIds = listOf(message.id),
|
||||||
|
isSpam = if (message.isOut) null else binding.check.isChecked,
|
||||||
|
deleteForAll = if (!binding.check.isEnabled) null else binding.check.isChecked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteMessages(event: MessagesDelete) {
|
||||||
|
adapter.removeMessagesByIds(event.messagesIds).let {
|
||||||
|
it.forEach { index -> adapter.notifyItemRemoved(index) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.show()
|
private fun editMessage(event: MessagesEdit) {
|
||||||
|
adapter.searchMessageIndex(event.message.id)?.let { index ->
|
||||||
|
adapter.values[index] = event.message
|
||||||
|
adapter.notifyItemChanged(index)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onItemLongClick(position: Int): Boolean {
|
private inner class AttachmentPanelController {
|
||||||
|
val isPanelVisible = MutableLiveData(false)
|
||||||
|
val message = MutableLiveData<VkMessage?>()
|
||||||
|
|
||||||
|
var isEditing = false
|
||||||
|
|
||||||
|
fun init(): AttachmentPanelController {
|
||||||
|
message.observe(viewLifecycleOwner) { value ->
|
||||||
|
if (value != null) {
|
||||||
|
applyMessage(value)
|
||||||
|
} else {
|
||||||
|
clearMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message.value = null
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.replyMessageTitle.text = title
|
||||||
|
binding.replyMessageText.text = message.text ?: "[no_message]"
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
binding.message.setText(message.text ?: "[no_message]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearMessage() {
|
||||||
|
hidePanel()
|
||||||
|
|
||||||
|
binding.replyMessageTitle.clear()
|
||||||
|
binding.replyMessageText.clear()
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
isEditing = false
|
||||||
|
binding.message.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPanel(duration: Long = 250) {
|
||||||
|
if (attachmentController.isPanelVisible.value == false)
|
||||||
|
attachmentController.isPanelVisible.value = true
|
||||||
|
|
||||||
|
binding.attachmentPanel.animate()
|
||||||
|
.translationY(0f)
|
||||||
|
.alpha(1f)
|
||||||
|
.setDuration(duration)
|
||||||
|
.withStartAction { binding.attachmentPanel.isVisible = true }
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hidePanel(duration: Long = 250) {
|
||||||
|
if (attachmentController.isPanelVisible.value == true)
|
||||||
|
attachmentController.isPanelVisible.value = false
|
||||||
|
|
||||||
|
binding.attachmentPanel.animate()
|
||||||
|
.alpha(0f)
|
||||||
|
.translationY(50f)
|
||||||
|
.setDuration(duration)
|
||||||
|
.withEndAction { binding.attachmentPanel.isVisible = false }
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -6,33 +6,29 @@ import com.meloda.fast.api.model.VkConversation
|
|||||||
import com.meloda.fast.api.model.VkGroup
|
import com.meloda.fast.api.model.VkGroup
|
||||||
import com.meloda.fast.api.model.VkMessage
|
import com.meloda.fast.api.model.VkMessage
|
||||||
import com.meloda.fast.api.model.VkUser
|
import com.meloda.fast.api.model.VkUser
|
||||||
import com.meloda.fast.api.model.request.MessagesGetHistoryRequest
|
import com.meloda.fast.api.model.attachments.VkAttachment
|
||||||
import com.meloda.fast.api.model.request.MessagesMarkAsImportantRequest
|
import com.meloda.fast.api.network.messages.*
|
||||||
import com.meloda.fast.api.model.request.MessagesSendRequest
|
|
||||||
import com.meloda.fast.api.network.datasource.MessagesDataSource
|
|
||||||
import com.meloda.fast.base.viewmodel.BaseViewModel
|
import com.meloda.fast.base.viewmodel.BaseViewModel
|
||||||
import com.meloda.fast.base.viewmodel.StartProgressEvent
|
import com.meloda.fast.base.viewmodel.VkEvent
|
||||||
import com.meloda.fast.base.viewmodel.StopProgressEvent
|
|
||||||
import com.meloda.fast.base.viewmodel.VKEvent
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MessagesHistoryViewModel @Inject constructor(
|
class MessagesHistoryViewModel @Inject constructor(
|
||||||
private val dataSource: MessagesDataSource
|
private val messages: MessagesDataSource
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
fun loadHistory(
|
fun loadHistory(
|
||||||
peerId: Int
|
peerId: Int
|
||||||
) = viewModelScope.launch {
|
) = viewModelScope.launch {
|
||||||
makeJob({
|
makeJob({
|
||||||
dataSource.getHistory(
|
messages.getHistory(
|
||||||
MessagesGetHistoryRequest(
|
MessagesGetHistoryRequest(
|
||||||
count = 30,
|
count = 30,
|
||||||
peerId = peerId,
|
peerId = peerId,
|
||||||
extended = true,
|
extended = true,
|
||||||
fields = "${VKConstants.USER_FIELDS},${VKConstants.GROUP_FIELDS}"
|
fields = VKConstants.ALL_FIELDS
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -53,18 +49,18 @@ class MessagesHistoryViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val messages = hashMapOf<Int, VkMessage>()
|
val hashMessages = hashMapOf<Int, VkMessage>()
|
||||||
response.items.forEach { baseMessage ->
|
response.items.forEach { baseMessage ->
|
||||||
baseMessage.asVkMessage().let { message -> messages[message.id] = message }
|
baseMessage.asVkMessage().let { message -> hashMessages[message.id] = message }
|
||||||
}
|
}
|
||||||
|
|
||||||
dataSource.storeMessages(messages.values.toList())
|
messages.store(hashMessages.values.toList())
|
||||||
|
|
||||||
val conversations = hashMapOf<Int, VkConversation>()
|
val conversations = hashMapOf<Int, VkConversation>()
|
||||||
response.conversations?.let { baseConversations ->
|
response.conversations?.let { baseConversations ->
|
||||||
baseConversations.forEach { baseConversation ->
|
baseConversations.forEach { baseConversation ->
|
||||||
baseConversation.asVkConversation(
|
baseConversation.asVkConversation(
|
||||||
messages[baseConversation.last_message_id]
|
hashMessages[baseConversation.last_message_id]
|
||||||
).let { conversation -> conversations[conversation.id] = conversation }
|
).let { conversation -> conversations[conversation.id] = conversation }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,41 +71,33 @@ class MessagesHistoryViewModel @Inject constructor(
|
|||||||
profiles = profiles,
|
profiles = profiles,
|
||||||
groups = groups,
|
groups = groups,
|
||||||
conversations = conversations,
|
conversations = conversations,
|
||||||
messages = messages.values.toList()
|
messages = hashMessages.values.toList()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
})
|
||||||
onError = {
|
|
||||||
val throwable = it
|
|
||||||
throw it
|
|
||||||
},
|
|
||||||
onStart = { sendEvent(StartProgressEvent) },
|
|
||||||
onEnd = { sendEvent(StopProgressEvent) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendMessage(
|
fun sendMessage(
|
||||||
peerId: Int,
|
peerId: Int,
|
||||||
message: String? = null,
|
message: String? = null,
|
||||||
randomId: Int = 0,
|
randomId: Int = 0,
|
||||||
|
replyTo: Int? = null,
|
||||||
setId: ((messageId: Int) -> Unit)? = null
|
setId: ((messageId: Int) -> Unit)? = null
|
||||||
) = viewModelScope.launch {
|
) = viewModelScope.launch {
|
||||||
makeJob(
|
makeJob(
|
||||||
{
|
{
|
||||||
dataSource.send(
|
messages.send(
|
||||||
MessagesSendRequest(
|
MessagesSendRequest(
|
||||||
peerId = peerId,
|
peerId = peerId,
|
||||||
randomId = randomId,
|
randomId = randomId,
|
||||||
message = message
|
message = message,
|
||||||
|
replyTo = replyTo
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onAnswer = {
|
onAnswer = {
|
||||||
val response = it.response ?: return@makeJob
|
val response = it.response ?: return@makeJob
|
||||||
setId?.invoke(response)
|
setId?.invoke(response)
|
||||||
},
|
|
||||||
onError = {
|
|
||||||
val throwable = it
|
|
||||||
val i = 0
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +106,7 @@ class MessagesHistoryViewModel @Inject constructor(
|
|||||||
important: Boolean
|
important: Boolean
|
||||||
) = viewModelScope.launch {
|
) = viewModelScope.launch {
|
||||||
makeJob({
|
makeJob({
|
||||||
dataSource.markAsImportant(
|
messages.markAsImportant(
|
||||||
MessagesMarkAsImportantRequest(
|
MessagesMarkAsImportantRequest(
|
||||||
messagesIds = messagesIds,
|
messagesIds = messagesIds,
|
||||||
important = important
|
important = important
|
||||||
@@ -133,13 +121,84 @@ class MessagesHistoryViewModel @Inject constructor(
|
|||||||
important = important
|
important = important
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
|
||||||
onError = {
|
|
||||||
val throwable = it
|
|
||||||
val i = 0
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun pinMessage(
|
||||||
|
peerId: Int,
|
||||||
|
messageId: Int? = null,
|
||||||
|
conversationMessageId: Int? = null,
|
||||||
|
pin: Boolean
|
||||||
|
) = viewModelScope.launch {
|
||||||
|
if (pin) {
|
||||||
|
makeJob({
|
||||||
|
messages.pin(
|
||||||
|
MessagesPinMessageRequest(
|
||||||
|
peerId = peerId,
|
||||||
|
messageId = messageId,
|
||||||
|
conversationMessageId = conversationMessageId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onAnswer = {
|
||||||
|
val response = it.response ?: return@makeJob
|
||||||
|
sendEvent(MessagesPin(response.asVkMessage()))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
makeJob({ messages.unpin(MessagesUnPinMessageRequest(peerId = peerId)) },
|
||||||
|
onAnswer = {
|
||||||
|
println("Fast::MessagesHistoryViewModel::unPin::Response::${it.response}")
|
||||||
|
sendEvent(MessagesUnpin)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteMessage(
|
||||||
|
peerId: Int,
|
||||||
|
messagesIds: List<Int>? = null,
|
||||||
|
conversationsMessagesIds: List<Int>? = null,
|
||||||
|
isSpam: Boolean? = null,
|
||||||
|
deleteForAll: Boolean? = null
|
||||||
|
) = viewModelScope.launch {
|
||||||
|
makeJob({
|
||||||
|
messages.delete(
|
||||||
|
MessagesDeleteRequest(
|
||||||
|
peerId = peerId,
|
||||||
|
messagesIds = messagesIds,
|
||||||
|
conversationsMessagesIds = conversationsMessagesIds,
|
||||||
|
isSpam = isSpam,
|
||||||
|
deleteForAll = deleteForAll
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}, onAnswer = { sendEvent(MessagesDelete(messagesIds = messagesIds ?: listOf())) })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun editMessage(
|
||||||
|
originalMessage: VkMessage,
|
||||||
|
peerId: Int,
|
||||||
|
messageId: Int,
|
||||||
|
message: String? = null,
|
||||||
|
attachments: List<VkAttachment>? = null
|
||||||
|
) = viewModelScope.launch {
|
||||||
|
makeJob(
|
||||||
|
{
|
||||||
|
messages.edit(
|
||||||
|
MessagesEditRequest(
|
||||||
|
peerId = peerId,
|
||||||
|
messageId = messageId,
|
||||||
|
message = message,
|
||||||
|
attachments = attachments
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onAnswer = {
|
||||||
|
originalMessage.text = message
|
||||||
|
sendEvent(MessagesEdit(originalMessage))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MessagesLoaded(
|
data class MessagesLoaded(
|
||||||
@@ -148,9 +207,23 @@ data class MessagesLoaded(
|
|||||||
val messages: List<VkMessage>,
|
val messages: List<VkMessage>,
|
||||||
val profiles: HashMap<Int, VkUser>,
|
val profiles: HashMap<Int, VkUser>,
|
||||||
val groups: HashMap<Int, VkGroup>
|
val groups: HashMap<Int, VkGroup>
|
||||||
) : VKEvent()
|
) : VkEvent()
|
||||||
|
|
||||||
data class MessagesMarkAsImportant(
|
data class MessagesMarkAsImportant(
|
||||||
val messagesIds: List<Int>,
|
val messagesIds: List<Int>,
|
||||||
val important: Boolean
|
val important: Boolean
|
||||||
) : VKEvent()
|
) : VkEvent()
|
||||||
|
|
||||||
|
data class MessagesPin(
|
||||||
|
val message: VkMessage
|
||||||
|
) : VkEvent()
|
||||||
|
|
||||||
|
object MessagesUnpin : VkEvent()
|
||||||
|
|
||||||
|
data class MessagesDelete(
|
||||||
|
val messagesIds: List<Int>
|
||||||
|
) : VkEvent()
|
||||||
|
|
||||||
|
data class MessagesEdit(
|
||||||
|
val message: VkMessage
|
||||||
|
) : VkEvent()
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.meloda.fast.screens.messages
|
package com.meloda.fast.screens.messages
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
@@ -8,9 +9,7 @@ import android.widget.Space
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.widget.LinearLayoutCompat
|
import androidx.appcompat.widget.LinearLayoutCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isInvisible
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.setPadding
|
|
||||||
import coil.load
|
import coil.load
|
||||||
import com.meloda.fast.R
|
import com.meloda.fast.R
|
||||||
import com.meloda.fast.api.VkUtils
|
import com.meloda.fast.api.VkUtils
|
||||||
@@ -20,7 +19,6 @@ import com.meloda.fast.api.model.VkMessage
|
|||||||
import com.meloda.fast.api.model.VkUser
|
import com.meloda.fast.api.model.VkUser
|
||||||
import com.meloda.fast.api.model.attachments.VkSticker
|
import com.meloda.fast.api.model.attachments.VkSticker
|
||||||
import com.meloda.fast.common.AppGlobal
|
import com.meloda.fast.common.AppGlobal
|
||||||
import com.meloda.fast.util.AndroidUtils
|
|
||||||
import com.meloda.fast.widget.BoundedLinearLayout
|
import com.meloda.fast.widget.BoundedLinearLayout
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -30,19 +28,21 @@ import kotlin.math.roundToInt
|
|||||||
class MessagesPreparator constructor(
|
class MessagesPreparator constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
|
||||||
|
private val root: View? = null,
|
||||||
|
|
||||||
private val conversation: VkConversation,
|
private val conversation: VkConversation,
|
||||||
private val message: VkMessage,
|
private val message: VkMessage,
|
||||||
private val prevMessage: VkMessage? = null,
|
private val prevMessage: VkMessage? = null,
|
||||||
private val nextMessage: VkMessage? = null,
|
private val nextMessage: VkMessage? = null,
|
||||||
|
|
||||||
private val bubble: BoundedLinearLayout? = null,
|
private val bubble: BoundedLinearLayout? = null,
|
||||||
private val bubbleStroke: View? = null,
|
|
||||||
private val text: TextView? = null,
|
private val text: TextView? = null,
|
||||||
private val avatar: ImageView? = null,
|
private val avatar: ImageView? = null,
|
||||||
private val title: TextView? = null,
|
private val title: TextView? = null,
|
||||||
private val spacer: Space? = null,
|
private val spacer: Space? = null,
|
||||||
private val unread: ImageView? = null,
|
private val unread: ImageView? = null,
|
||||||
private val time: TextView? = null,
|
private val time: TextView? = null,
|
||||||
|
private val textContainer: LinearLayoutCompat? = null,
|
||||||
private val attachmentContainer: LinearLayoutCompat? = null,
|
private val attachmentContainer: LinearLayoutCompat? = null,
|
||||||
private val attachmentSpacer: Space? = null,
|
private val attachmentSpacer: Space? = null,
|
||||||
|
|
||||||
@@ -65,51 +65,129 @@ class MessagesPreparator constructor(
|
|||||||
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background)
|
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background)
|
||||||
private val backgroundMiddleOut =
|
private val backgroundMiddleOut =
|
||||||
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle)
|
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle)
|
||||||
private val backgroundStrokeOut =
|
|
||||||
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_stroke)
|
private val rootHighlightedColor =
|
||||||
private val backgroundMiddleStrokeOut =
|
ContextCompat.getColor(context, R.color.n2_100)
|
||||||
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle_stroke)
|
|
||||||
|
private var photoClickListener: ((url: String) -> Unit)? = null
|
||||||
|
|
||||||
|
fun setPhotoClickListener(unit: ((url: String) -> Unit)?): MessagesPreparator {
|
||||||
|
this.photoClickListener = unit
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
fun prepare() {
|
fun prepare() {
|
||||||
val messageUser: VkUser? = if (message.isUser()) {
|
val messageUser = VkUtils.getMessageUser(message, profiles)
|
||||||
profiles[message.fromId]
|
val messageGroup = VkUtils.getMessageGroup(message, groups)
|
||||||
} else null
|
|
||||||
|
|
||||||
val messageGroup: VkGroup? = if (message.isGroup()) {
|
prepareRootBackground()
|
||||||
groups[message.fromId]
|
|
||||||
} else null
|
|
||||||
|
|
||||||
|
prepareTime()
|
||||||
|
|
||||||
|
prepareUnreadIndicator()
|
||||||
|
|
||||||
|
prepareSpacer()
|
||||||
|
|
||||||
|
prepareAttachments()
|
||||||
|
|
||||||
|
prepareAttachmentsSpacer()
|
||||||
|
|
||||||
|
prepareBubbleBackground()
|
||||||
|
|
||||||
|
prepareText()
|
||||||
|
|
||||||
|
prepareAvatar(
|
||||||
|
messageUser = messageUser,
|
||||||
|
messageGroup = messageGroup
|
||||||
|
)
|
||||||
|
|
||||||
|
if (message.isPeerChat()) {
|
||||||
|
val prevSenderDiff = VkUtils.isPreviousMessageFromDifferentSender(prevMessage, message)
|
||||||
|
val nextSenderDiff = VkUtils.isPreviousMessageFromDifferentSender(message, nextMessage)
|
||||||
|
val fiveMinAgo = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message)
|
||||||
|
|
||||||
|
val change = (prevMessage?.date ?: 0) - message.date
|
||||||
|
|
||||||
|
Log.d(
|
||||||
|
"Fast::MessagesPreparator",
|
||||||
|
"text: ${message.text}; prevText: ${prevMessage?.text}; time change: $change; fromDiffSender: $prevSenderDiff; fiveMinAgo: $fiveMinAgo; "
|
||||||
|
)
|
||||||
|
|
||||||
|
title?.isVisible = prevSenderDiff || fiveMinAgo
|
||||||
|
|
||||||
|
avatar?.visibility =
|
||||||
|
if (nextSenderDiff
|
||||||
|
|| (fiveMinAgo && prevSenderDiff)
|
||||||
|
|| !prevSenderDiff
|
||||||
|
|| nextMessage == null
|
||||||
|
) View.VISIBLE else View.INVISIBLE
|
||||||
|
} else {
|
||||||
|
title?.isVisible = false
|
||||||
|
avatar?.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (title != null) {
|
||||||
|
val titleString = when {
|
||||||
|
message.isUser() && messageUser != null -> messageUser.firstName
|
||||||
|
message.isGroup() && messageGroup != null -> messageGroup.name
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
title.text = titleString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prepareRootBackground() {
|
||||||
|
if (root != null) {
|
||||||
|
root.background =
|
||||||
|
if (message.isSelected) ColorDrawable(rootHighlightedColor)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prepareTime() {
|
||||||
|
time?.text = SimpleDateFormat("HH:mm", Locale.getDefault()).format(message.date * 1000L)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prepareUnreadIndicator() {
|
||||||
if (unread != null) {
|
if (unread != null) {
|
||||||
unread.isVisible = message.isRead(conversation)
|
unread.isVisible = message.isRead(conversation)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bubble != null && time != null) {
|
|
||||||
bubble.setOnClickListener { time.isVisible = !time.isVisible }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachmentContainer != null) {
|
private fun prepareSpacer() {
|
||||||
|
spacer?.isVisible = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prepareAttachments() {
|
||||||
|
if (attachmentContainer != null && textContainer != null) {
|
||||||
if (message.attachments.isNullOrEmpty()) {
|
if (message.attachments.isNullOrEmpty()) {
|
||||||
attachmentContainer.isVisible = false
|
attachmentContainer.isVisible = false
|
||||||
attachmentContainer.removeAllViews()
|
attachmentContainer.removeAllViews()
|
||||||
} else {
|
} else {
|
||||||
attachmentContainer.isVisible = true
|
attachmentContainer.isVisible = true
|
||||||
|
|
||||||
AttachmentInflater(
|
AttachmentInflater(
|
||||||
context = context,
|
context = context,
|
||||||
container = attachmentContainer,
|
container = attachmentContainer,
|
||||||
|
textContainer = textContainer,
|
||||||
message = message,
|
message = message,
|
||||||
groups = groups,
|
groups = groups,
|
||||||
profiles = profiles
|
profiles = profiles
|
||||||
).inflate()
|
)
|
||||||
|
.setPhotoClickListener(photoClickListener)
|
||||||
|
.inflate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun prepareAttachmentsSpacer() {
|
||||||
|
attachmentSpacer?.isVisible =
|
||||||
|
!message.attachments.isNullOrEmpty() && text?.isVisible == true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prepareBubbleBackground() {
|
||||||
if (bubble != null) {
|
if (bubble != null) {
|
||||||
val padding =
|
|
||||||
AndroidUtils.px(if (!message.attachments.isNullOrEmpty()) 4 else 15).roundToInt()
|
|
||||||
|
|
||||||
bubble.setPadding(padding)
|
|
||||||
|
|
||||||
|
|
||||||
// TODO: 9/23/2021 use external function
|
// TODO: 9/23/2021 use external function
|
||||||
bubble.background =
|
bubble.background =
|
||||||
if (!message.attachments.isNullOrEmpty() && message.attachments!![0] is VkSticker) null
|
if (!message.attachments.isNullOrEmpty() && message.attachments!![0] is VkSticker) null
|
||||||
@@ -125,82 +203,29 @@ class MessagesPreparator constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 9/23/2021 use external function
|
|
||||||
bubbleStroke?.background =
|
|
||||||
if (bubble?.background == null) null else {
|
|
||||||
if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundStrokeOut
|
|
||||||
else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleStrokeOut
|
|
||||||
else backgroundStrokeOut
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun prepareText() {
|
||||||
if (bubble != null && text != null) {
|
if (bubble != null && text != null) {
|
||||||
if (message.text == null) {
|
if (message.text == null) {
|
||||||
text.isVisible = false
|
text.isVisible = false
|
||||||
bubble.isVisible = !message.attachments.isNullOrEmpty()
|
bubble.isVisible = !message.attachments.isNullOrEmpty()
|
||||||
bubbleStroke?.isVisible = bubble.isVisible
|
|
||||||
} else {
|
} else {
|
||||||
text.isVisible = true
|
text.isVisible = true
|
||||||
bubble.isVisible = true
|
bubble.isVisible = true
|
||||||
bubbleStroke?.isVisible = true
|
text.text = VkUtils.prepareMessageText(message.text ?: "")
|
||||||
text.text = VkUtils.prepareMessageText(message.text)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun prepareAvatar(
|
||||||
|
messageUser: VkUser? = null,
|
||||||
|
messageGroup: VkGroup? = null
|
||||||
|
) {
|
||||||
if (avatar != null) {
|
if (avatar != null) {
|
||||||
val avatarUrl = when {
|
val avatarUrl = VkUtils.getMessageAvatar(message, messageUser, messageGroup)
|
||||||
message.isUser() && messageUser != null && !messageUser.photo200.isNullOrBlank() -> messageUser.photo200
|
|
||||||
message.isGroup() && messageGroup != null && !messageGroup.photo200.isNullOrBlank() -> messageGroup.photo200
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
avatar.load(avatarUrl) { crossfade(100) }
|
avatar.load(avatarUrl) { crossfade(100) }
|
||||||
}
|
}
|
||||||
|
|
||||||
spacer?.isVisible = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message)
|
|
||||||
|
|
||||||
if (message.isPeerChat()) {
|
|
||||||
|
|
||||||
val fromDiffSender = VkUtils.isPreviousMessageFromDifferentSender(prevMessage, message)
|
|
||||||
val fiveMinAgo = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message)
|
|
||||||
|
|
||||||
val change = (prevMessage?.date ?: 0) - message.date
|
|
||||||
|
|
||||||
Log.d(
|
|
||||||
"Fast::MessagesPreparator",
|
|
||||||
"text: ${message.text}; prevText: ${prevMessage?.text}; time change: $change; fromDiffSender: $fromDiffSender; fiveMinAgo: $fiveMinAgo; "
|
|
||||||
)
|
|
||||||
|
|
||||||
title?.isVisible = fromDiffSender || fiveMinAgo
|
|
||||||
|
|
||||||
avatar?.isInvisible = fromDiffSender && fiveMinAgo
|
|
||||||
} else {
|
|
||||||
title?.isVisible = false
|
|
||||||
avatar?.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (title != null) {
|
|
||||||
val titleString = when {
|
|
||||||
message.isUser() && messageUser != null -> messageUser.firstName
|
|
||||||
message.isGroup() && messageGroup != null -> messageGroup.name
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
title.text = titleString
|
|
||||||
title.measure(0, 0)
|
|
||||||
|
|
||||||
if (bubble != null) {
|
|
||||||
if (title.isVisible) {
|
|
||||||
bubble.minimumWidth = title.measuredWidth + 60
|
|
||||||
} else {
|
|
||||||
bubble.minimumWidth = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
attachmentSpacer?.isVisible =
|
|
||||||
!message.attachments.isNullOrEmpty() && text?.isVisible == true
|
|
||||||
|
|
||||||
time?.text = SimpleDateFormat("HH:mm", Locale.getDefault()).format(message.date * 1000L)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.meloda.fast.screens.photos
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import com.meloda.fast.base.BaseViewModelFragment
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class PhotoViewFragment : BaseViewModelFragment<PhotoViewViewModel>() {
|
||||||
|
|
||||||
|
override val viewModel: PhotoViewViewModel by viewModels()
|
||||||
|
|
||||||
|
// private val photosList: MutableList<VkPhoto> = mutableListOf()
|
||||||
|
|
||||||
|
private var photoLink: String? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
photoLink = requireArguments().getString("photoLink")
|
||||||
|
|
||||||
|
// val list: List<*>? = Gson().fromJson(
|
||||||
|
// requireArguments().getString("photosList"),
|
||||||
|
// List::class.java
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// list?.forEach { if (it is VkPhoto) photosList.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
return ImageView(requireContext())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
photoLink?.let { viewModel.loadImageFromUrl(it, requireView() as ImageView) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.meloda.fast.screens.photos
|
||||||
|
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import coil.load
|
||||||
|
import com.meloda.fast.base.viewmodel.BaseViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class PhotoViewViewModel : BaseViewModel() {
|
||||||
|
|
||||||
|
fun loadImageFromUrl(
|
||||||
|
url: String,
|
||||||
|
imageView: ImageView
|
||||||
|
) = viewModelScope.launch {
|
||||||
|
imageView.load(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveImageToLocalStorage(url: String) = viewModelScope.launch {
|
||||||
|
TODO("Not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.meloda.fast.service
|
package com.meloda.fast.service
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.meloda.fast.api.model.request.MessagesGetLongPollServerRequest
|
import com.meloda.fast.api.network.messages.MessagesGetLongPollServerRequest
|
||||||
import com.meloda.fast.api.network.datasource.MessagesDataSource
|
import com.meloda.fast.api.network.messages.MessagesDataSource
|
||||||
import com.meloda.fast.api.network.repo.LongPollRepo
|
import com.meloda.fast.api.network.longpoll.LongPollRepo
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
package com.meloda.fast.util
|
|
||||||
|
|
||||||
import java.util.stream.Collectors
|
|
||||||
|
|
||||||
object ArrayUtils {
|
|
||||||
|
|
||||||
@SafeVarargs
|
|
||||||
fun <T> asString(vararg array: T): String {
|
|
||||||
if (array.isEmpty()) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
val builder = StringBuilder(array.size * 12)
|
|
||||||
builder.append(array[0])
|
|
||||||
for (i in 1 until array.size) {
|
|
||||||
builder.append(',')
|
|
||||||
builder.append(array[i])
|
|
||||||
}
|
|
||||||
return builder.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun asString(array: IntArray): String {
|
|
||||||
if (array.isEmpty()) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
val builder = StringBuilder(array.size * 12)
|
|
||||||
builder.append(array[0])
|
|
||||||
for (i in 1 until array.size) {
|
|
||||||
builder.append(',')
|
|
||||||
builder.append(array[i])
|
|
||||||
}
|
|
||||||
return builder.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> asString(arrayList: ArrayList<T>): String {
|
|
||||||
return ArrayList<String>().apply {
|
|
||||||
arrayList.forEach { add(it.toString()) }
|
|
||||||
}.stream().collect(Collectors.joining(","))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> asString(list: List<T>): String = asString(list.asArrayList())
|
|
||||||
|
|
||||||
fun <T> cut(arrayList: ArrayList<T>, offset: Int, count: Int): ArrayList<T> {
|
|
||||||
if (arrayList.isEmpty()) return arrayListOf()
|
|
||||||
|
|
||||||
var lastPosition = offset + count
|
|
||||||
if (lastPosition > arrayList.size) lastPosition = arrayList.size
|
|
||||||
|
|
||||||
return ArrayList(arrayList.subList(offset, lastPosition))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ByteArray?.isNullOrEmpty() = this == null || this.isEmpty()
|
|
||||||
|
|
||||||
fun <E> List<E>.asArrayList(): ArrayList<E> {
|
|
||||||
return ArrayList(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package com.meloda.fast.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Color
|
|
||||||
import androidx.annotation.ColorInt
|
|
||||||
import com.meloda.fast.R
|
|
||||||
|
|
||||||
object ColorUtils {
|
|
||||||
|
|
||||||
@ColorInt
|
|
||||||
fun getColorAccent(context: Context): Int {
|
|
||||||
return AndroidUtils.getThemeAttrColor(context, R.attr.colorAccent)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ColorInt
|
|
||||||
fun getColorPrimary(context: Context): Int {
|
|
||||||
return AndroidUtils.getThemeAttrColor(context, R.attr.colorPrimary)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmOverloads
|
|
||||||
fun darkenColor(color: Int, darkFactor: Float = 0.75f): Int {
|
|
||||||
var newColor = color
|
|
||||||
val hsv = FloatArray(3)
|
|
||||||
Color.colorToHSV(newColor, hsv)
|
|
||||||
hsv[2] *= darkFactor
|
|
||||||
newColor = Color.HSVToColor(hsv)
|
|
||||||
return newColor
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package com.meloda.fast.util
|
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.widget.ImageView
|
|
||||||
|
|
||||||
object ImageUtils {
|
|
||||||
|
|
||||||
fun loadImage(image: String, imageView: ImageView, placeholder: Drawable?) {
|
|
||||||
if (image.isEmpty()) return
|
|
||||||
|
|
||||||
// if (imageView is SimpleDraweeView) {
|
|
||||||
// imageView.setImageURI(image)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// val picasso = Picasso.get()
|
|
||||||
// .load(image)
|
|
||||||
// .priority(Picasso.Priority.LOW)
|
|
||||||
|
|
||||||
// if (placeholder != null) picasso.placeholder(placeholder)
|
|
||||||
//
|
|
||||||
// picasso.into(imageView)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadImage(image: String?, listener: OnLoadListener?) {
|
|
||||||
if (image.isNullOrEmpty()) return
|
|
||||||
|
|
||||||
// val picasso = Picasso.get()
|
|
||||||
// .load(image)
|
|
||||||
// .priority(Picasso.Priority.LOW)
|
|
||||||
//
|
|
||||||
// val target = object : Target {
|
|
||||||
// override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun onBitmapFailed(e: Exception, errorDrawable: Drawable?) {
|
|
||||||
// listener?.onError(e)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
|
|
||||||
// listener?.onLoad(bitmap)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// picasso.into(target)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface OnLoadListener {
|
|
||||||
fun onLoad(bitmap: Bitmap)
|
|
||||||
fun onError(e: Exception)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.meloda.fast.util
|
|
||||||
|
|
||||||
object TextUtils {
|
|
||||||
|
|
||||||
fun getFirstLetterFromString(string: String): String {
|
|
||||||
for (i in string.indices) {
|
|
||||||
val char = string[i]
|
|
||||||
|
|
||||||
if (char.isLetter()) return char.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -7,6 +7,8 @@ import java.util.*
|
|||||||
|
|
||||||
object TimeUtils {
|
object TimeUtils {
|
||||||
|
|
||||||
|
const val ONE_DAY_IN_SECONDS = 86400
|
||||||
|
|
||||||
fun removeTime(date: Date): Long {
|
fun removeTime(date: Date): Long {
|
||||||
return Calendar.getInstance().apply {
|
return Calendar.getInstance().apply {
|
||||||
time = date
|
time = date
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
package com.meloda.fast.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.meloda.fast.util.ArrayUtils.isNullOrEmpty
|
|
||||||
import com.meloda.fast.R
|
|
||||||
import com.meloda.fast.io.BytesOutputStream
|
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.ObjectInputStream
|
|
||||||
import java.io.ObjectOutputStream
|
|
||||||
|
|
||||||
object Utils {
|
|
||||||
|
|
||||||
fun getLocalizedThrowable(context: Context, t: Throwable): String {
|
|
||||||
return context.getString(R.string.error, t.message.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun serialize(source: Any?): ByteArray? {
|
|
||||||
try {
|
|
||||||
val bos = BytesOutputStream()
|
|
||||||
val out = ObjectOutputStream(bos)
|
|
||||||
out.writeObject(source)
|
|
||||||
out.close()
|
|
||||||
return bos.byteArray
|
|
||||||
} catch (e: IOException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deserialize(source: ByteArray?): Any? {
|
|
||||||
if (source.isNullOrEmpty()) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val bis = ByteArrayInputStream(source)
|
|
||||||
val `in` = ObjectInputStream(bis)
|
|
||||||
val o = `in`.readObject()
|
|
||||||
`in`.close()
|
|
||||||
return o
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -10,12 +10,11 @@ import android.widget.TextView
|
|||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.meloda.fast.extensions.ContextExtensions.drawable
|
|
||||||
import com.meloda.fast.extensions.DrawableExtensions.tint
|
|
||||||
import com.meloda.fast.extensions.FloatExtensions.int
|
|
||||||
import com.meloda.fast.R
|
import com.meloda.fast.R
|
||||||
import com.meloda.fast.util.AndroidUtils
|
import com.meloda.fast.util.AndroidUtils
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
class NoItemsView @JvmOverloads constructor(
|
class NoItemsView @JvmOverloads constructor(
|
||||||
@@ -44,7 +43,7 @@ class NoItemsView @JvmOverloads constructor(
|
|||||||
private fun create() {
|
private fun create() {
|
||||||
val a = context.obtainStyledAttributes(attrs, R.styleable.NoItemsView)
|
val a = context.obtainStyledAttributes(attrs, R.styleable.NoItemsView)
|
||||||
|
|
||||||
minimumWidth = AndroidUtils.px(256).int()
|
minimumWidth = AndroidUtils.px(256).roundToInt()
|
||||||
minimumHeight = minimumWidth
|
minimumHeight = minimumWidth
|
||||||
|
|
||||||
orientation = VERTICAL
|
orientation = VERTICAL
|
||||||
@@ -53,8 +52,8 @@ class NoItemsView @JvmOverloads constructor(
|
|||||||
noItemsPicture = ImageView(context)
|
noItemsPicture = ImageView(context)
|
||||||
|
|
||||||
val params = imageViewParams
|
val params = imageViewParams
|
||||||
params.height = AndroidUtils.px(64).int()
|
params.height = AndroidUtils.px(64).roundToInt()
|
||||||
params.width = AndroidUtils.px(64).int()
|
params.width = AndroidUtils.px(64).roundToInt()
|
||||||
|
|
||||||
noItemsPicture.layoutParams = params
|
noItemsPicture.layoutParams = params
|
||||||
|
|
||||||
@@ -73,10 +72,10 @@ class NoItemsView @JvmOverloads constructor(
|
|||||||
noItemsTextView = TextView(context)
|
noItemsTextView = TextView(context)
|
||||||
|
|
||||||
val textParams = textViewParams
|
val textParams = textViewParams
|
||||||
textParams.width = AndroidUtils.px(256).int()
|
textParams.width = AndroidUtils.px(256).roundToInt()
|
||||||
|
|
||||||
if (noItemsDrawable != null) {
|
if (noItemsDrawable != null) {
|
||||||
textParams.topMargin = AndroidUtils.px(8).int()
|
textParams.topMargin = AndroidUtils.px(8).roundToInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
noItemsTextView.layoutParams = textParams
|
noItemsTextView.layoutParams = textParams
|
||||||
@@ -103,7 +102,7 @@ class NoItemsView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setNoItemsImage(@DrawableRes resId: Int) {
|
fun setNoItemsImage(@DrawableRes resId: Int) {
|
||||||
setNoItemsImage(context.drawable(resId))
|
setNoItemsImage(AppCompatResources.getDrawable(context, resId))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setNoItemsImage(drawable: Drawable?) {
|
fun setNoItemsImage(drawable: Drawable?) {
|
||||||
@@ -111,7 +110,7 @@ class NoItemsView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setNoItemsImageTint(@ColorInt color: Int) {
|
fun setNoItemsImageTint(@ColorInt color: Int) {
|
||||||
noItemsPicture.drawable.tint(color)
|
noItemsPicture.drawable?.setTint(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setNoItemsText(@StringRes resId: Int) {
|
fun setNoItemsText(@StringRes resId: Int) {
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
android:width="24dp"
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
android:height="24dp"
|
||||||
<path android:fillColor="#FF000000" android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
|
android:tint="#FFFFFF"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<corners
|
||||||
|
android:topLeftRadius="30dp"
|
||||||
|
android:topRightRadius="30dp" />
|
||||||
|
|
||||||
|
<solid android:color="@android:color/white" />
|
||||||
|
|
||||||
|
</shape>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user