forked from melod1n/fast-messenger
+19
-10
@@ -7,9 +7,9 @@ plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
id("kotlin-kapt")
|
||||
id("kotlin-parcelize")
|
||||
id("androidx.navigation.safeargs.kotlin")
|
||||
id("dagger.hilt.android.plugin")
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -38,6 +38,9 @@ android {
|
||||
getByName("release") {
|
||||
isMinifyEnabled = false
|
||||
|
||||
buildConfigField("String", "vkLogin", login)
|
||||
buildConfigField("String", "vkPassword", password)
|
||||
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
@@ -68,7 +71,7 @@ android {
|
||||
kapt {
|
||||
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 {
|
||||
option("-Adagger.hilt.android.internal.disableAndroidSuperclassValidation=true")
|
||||
}
|
||||
@@ -79,13 +82,19 @@ dependencies {
|
||||
|
||||
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.datastore:datastore-preferences:1.0.0")
|
||||
|
||||
implementation("androidx.appcompat:appcompat:1.4.0-alpha03")
|
||||
implementation("com.google.android.material:material:1.5.0-alpha03")
|
||||
implementation("androidx.core:core-ktx:1.7.0-beta01")
|
||||
implementation("androidx.paging:paging-runtime-ktx:3.0.1")
|
||||
|
||||
implementation("androidx.appcompat:appcompat:1.4.0-beta01")
|
||||
implementation("com.google.android.material:material:1.5.0-alpha04")
|
||||
implementation("androidx.core:core-ktx:1.7.0-beta02")
|
||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
||||
@@ -93,7 +102,7 @@ dependencies {
|
||||
implementation("androidx.fragment:fragment-ktx:1.3.6")
|
||||
|
||||
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-runtime:2.3.0")
|
||||
@@ -113,16 +122,16 @@ dependencies {
|
||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
|
||||
|
||||
implementation("com.google.dagger:hilt-android:2.38.1")
|
||||
kapt("com.google.dagger:hilt-android-compiler:2.38.1")
|
||||
implementation("com.google.dagger:hilt-android:2.39.1")
|
||||
kapt("com.google.dagger:hilt-android-compiler:2.39.1")
|
||||
implementation("androidx.hilt:hilt-navigation-fragment:1.0.0")
|
||||
|
||||
implementation("com.github.yogacp:android-viewbinding:1.0.3")
|
||||
|
||||
implementation("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("org.jsoup:jsoup:1.14.2")
|
||||
implementation("org.jsoup:jsoup:1.14.3")
|
||||
implementation("ch.acra:acra:4.11.1")
|
||||
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
<application
|
||||
android:name=".common.AppGlobal"
|
||||
android:allowBackup="false"
|
||||
android:extractNativeLibs="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
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.base.BaseActivity
|
||||
import dagger.hilt.android.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 {
|
||||
|
||||
private const val FAST_TOKEN = "fast_token"
|
||||
private const val TOKEN = "token"
|
||||
private const val USER_ID = "user_id"
|
||||
|
||||
@@ -25,8 +26,16 @@ object UserConfig {
|
||||
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() {
|
||||
accessToken = ""
|
||||
fastToken = ""
|
||||
userId = -1
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.meloda.fast.api
|
||||
|
||||
import com.meloda.fast.api.model.attachments.*
|
||||
|
||||
object VKConstants {
|
||||
|
||||
const val GROUP_FIELDS = "description,members_count,counters,status,verified"
|
||||
@@ -7,6 +9,8 @@ object VKConstants {
|
||||
const val USER_FIELDS =
|
||||
"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 VK_APP_ID = "2274003"
|
||||
const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH"
|
||||
@@ -33,4 +37,15 @@ object VKConstants {
|
||||
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 description: String = "",
|
||||
var error: String
|
||||
) :
|
||||
IOException(description) {
|
||||
) : IOException(description) {
|
||||
|
||||
var captcha: Pair<String, String>? = null
|
||||
var validationSid: String? = null
|
||||
// TODO: 10-Oct-21 remove this
|
||||
var json: JSONObject? = null
|
||||
|
||||
override fun toString(): String {
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.text.SpannableString
|
||||
import android.text.style.StyleSpan
|
||||
import androidx.core.content.ContextCompat
|
||||
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.VkMessage
|
||||
import com.meloda.fast.api.model.VkUser
|
||||
@@ -16,6 +17,88 @@ import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
|
||||
|
||||
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 {
|
||||
return text.apply {
|
||||
if (forConversations == true) replace("\n", "")
|
||||
@@ -42,6 +125,12 @@ object VkUtils {
|
||||
return forwards
|
||||
}
|
||||
|
||||
fun parseReplyMessage(baseReplyMessage: BaseVkMessage?): VkMessage? {
|
||||
if (baseReplyMessage == null) return null
|
||||
|
||||
return baseReplyMessage.asVkMessage()
|
||||
}
|
||||
|
||||
fun parseAttachments(baseAttachments: List<BaseVkAttachmentItem>?): List<VkAttachment>? {
|
||||
if (baseAttachments.isNullOrEmpty()) return null
|
||||
|
||||
@@ -77,9 +166,7 @@ object VkUtils {
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.VOICE -> {
|
||||
val voiceMessage = baseAttachment.voiceMessage ?: continue
|
||||
attachments += VkVoiceMessage(
|
||||
link = voiceMessage.link_mp3
|
||||
)
|
||||
attachments += voiceMessage.asVkVoiceMessage()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.STICKER -> {
|
||||
val sticker = baseAttachment.sticker ?: continue
|
||||
@@ -87,9 +174,7 @@ object VkUtils {
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.GIFT -> {
|
||||
val gift = baseAttachment.gift ?: continue
|
||||
attachments += VkGift(
|
||||
link = gift.thumb_48
|
||||
)
|
||||
attachments += gift.asVkGift()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.WALL -> {
|
||||
val wall = baseAttachment.wall ?: continue
|
||||
@@ -97,9 +182,7 @@ object VkUtils {
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.GRAFFITI -> {
|
||||
val graffiti = baseAttachment.graffiti ?: continue
|
||||
attachments += VkGraffiti(
|
||||
link = graffiti.url
|
||||
)
|
||||
attachments += graffiti.asVkGraffiti()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.POLL -> {
|
||||
val poll = baseAttachment.poll ?: continue
|
||||
@@ -115,9 +198,7 @@ object VkUtils {
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.CALL -> {
|
||||
val call = baseAttachment.call ?: continue
|
||||
attachments += VkCall(
|
||||
initiatorId = call.initiator_id
|
||||
)
|
||||
attachments += call.asVkCall()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS -> {
|
||||
val groupCall = baseAttachment.groupCall ?: continue
|
||||
|
||||
@@ -1,38 +1,50 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Entity(tableName = "conversations")
|
||||
@Parcelize
|
||||
data class VkConversation(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
val id: Int,
|
||||
val ownerId: Int?,
|
||||
val title: String?,
|
||||
val photo200: String?,
|
||||
val type: String,
|
||||
val callInProgress: Boolean,
|
||||
val isPhantom: Boolean,
|
||||
val lastConversationMessageId: Int,
|
||||
val inRead: Int,
|
||||
val outRead: Int,
|
||||
val isMarkedUnread: Boolean,
|
||||
val lastMessageId: Int,
|
||||
val unreadCount: Int?,
|
||||
val membersCount: Int?,
|
||||
val isPinned: Boolean,
|
||||
var id: Int,
|
||||
var ownerId: Int?,
|
||||
var title: String?,
|
||||
var photo200: String?,
|
||||
var type: String,
|
||||
var callInProgress: Boolean,
|
||||
var isPhantom: Boolean,
|
||||
var lastConversationMessageId: Int,
|
||||
var inRead: Int,
|
||||
var outRead: Int,
|
||||
var isMarkedUnread: Boolean,
|
||||
var lastMessageId: Int,
|
||||
var unreadCount: Int?,
|
||||
var membersCount: Int?,
|
||||
var isPinned: Boolean,
|
||||
var canChangePin: Boolean,
|
||||
|
||||
@Embedded(prefix = "pinnedMessage_")
|
||||
var pinnedMessage: VkMessage? = null,
|
||||
|
||||
@Embedded(prefix = "lastMessage_")
|
||||
var lastMessage: VkMessage? = null
|
||||
var lastMessage: VkMessage? = null,
|
||||
) : Parcelable {
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
val user = MutableLiveData<VkUser?>()
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
val group = MutableLiveData<VkGroup?>()
|
||||
|
||||
fun isChat() = type == "chat"
|
||||
fun isUser() = type == "user"
|
||||
fun isGroup() = type == "group"
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.PrimaryKey
|
||||
import com.meloda.fast.api.UserConfig
|
||||
import com.meloda.fast.api.VKConstants
|
||||
import com.meloda.fast.api.model.attachments.VkAttachment
|
||||
import com.meloda.fast.base.adapter.SelectableItem
|
||||
import com.meloda.fast.util.TimeUtils
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Entity(tableName = "messages")
|
||||
@Parcelize
|
||||
data class VkMessage(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
val id: Int,
|
||||
val text: String? = null,
|
||||
var id: Int,
|
||||
var text: String? = null,
|
||||
val isOut: Boolean,
|
||||
val peerId: Int,
|
||||
val fromId: Int,
|
||||
@@ -23,10 +29,22 @@ data class VkMessage(
|
||||
val actionConversationMessageId: Int? = null,
|
||||
val actionMessage: String? = null,
|
||||
val geoType: String? = null,
|
||||
val important: Boolean = false,
|
||||
var important: Boolean = false,
|
||||
|
||||
var forwards: List<VkMessage>? = null,
|
||||
var attachments: List<VkAttachment>? = null
|
||||
) : Parcelable {
|
||||
var attachments: List<VkAttachment>? = null,
|
||||
|
||||
// @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
|
||||
|
||||
@@ -43,40 +61,12 @@ data class VkMessage(
|
||||
return Action.parse(action)
|
||||
}
|
||||
|
||||
fun copyMessage(
|
||||
id: Int = this.id,
|
||||
text: String? = this.text,
|
||||
isOut: Boolean = this.isOut,
|
||||
peerId: Int = this.peerId,
|
||||
fromId: Int = this.fromId,
|
||||
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
|
||||
}
|
||||
fun canEdit() =
|
||||
fromId == UserConfig.userId &&
|
||||
(attachments == null || !VKConstants.restrictedToEditAttachments.contains(
|
||||
attachments!![0].javaClass
|
||||
)) &&
|
||||
(System.currentTimeMillis() / 1000 - date.toLong() < TimeUtils.ONE_DAY_IN_SECONDS)
|
||||
|
||||
enum class Action(val value: String) {
|
||||
CHAT_CREATE("chat_create"),
|
||||
|
||||
@@ -4,4 +4,8 @@ import android.os.Parcelable
|
||||
import kotlinx.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
|
||||
|
||||
import com.meloda.fast.api.VkUtils
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkAudio(
|
||||
val id: Int,
|
||||
val ownerId: Int,
|
||||
val title: String,
|
||||
val artist: String,
|
||||
val url: String,
|
||||
val duration: Int
|
||||
val duration: Int,
|
||||
val accessKey: String?
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
|
||||
override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString(
|
||||
attachmentClass = this::class.java,
|
||||
id = id,
|
||||
ownerId = ownerId,
|
||||
withAccessKey = withAccessKey,
|
||||
accessKey = accessKey
|
||||
)
|
||||
}
|
||||
@@ -5,7 +5,12 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
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() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import com.meloda.fast.api.VkUtils
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkFile(
|
||||
val id: Int,
|
||||
val ownerId: Int,
|
||||
val title: String,
|
||||
val ext: String,
|
||||
val size: Int,
|
||||
val url: String
|
||||
val url: String,
|
||||
val accessKey: String?
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
|
||||
override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString(
|
||||
attachmentClass = this::class.java,
|
||||
id = id,
|
||||
ownerId = ownerId,
|
||||
withAccessKey = withAccessKey,
|
||||
accessKey = accessKey
|
||||
)
|
||||
}
|
||||
@@ -5,7 +5,10 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkGift(
|
||||
val link: String
|
||||
val id: Int,
|
||||
val thumb256: String?,
|
||||
val thumb96: String?,
|
||||
val thumb48: String
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
|
||||
@@ -5,7 +5,12 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
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() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
|
||||
@@ -9,7 +9,7 @@ data class VkLink(
|
||||
val title: String?,
|
||||
val caption: String?,
|
||||
val photo: VkPhoto?,
|
||||
val target: String,
|
||||
val target: String?,
|
||||
val isFavorite: Boolean
|
||||
) : VkAttachment() {
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
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 kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.util.*
|
||||
|
||||
@Parcelize
|
||||
data class VkPhoto(
|
||||
@@ -13,17 +16,70 @@ data class VkPhoto(
|
||||
val hasTags: Boolean,
|
||||
val accessKey: String?,
|
||||
val sizes: List<BaseVkPhoto.Size>,
|
||||
val text: String,
|
||||
val text: String?,
|
||||
val userId: Int?
|
||||
) : 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
|
||||
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) {
|
||||
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 null
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
import com.meloda.fast.api.VkUtils
|
||||
import com.meloda.fast.api.model.base.attachments.BaseVkVideo
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@@ -7,8 +8,10 @@ import kotlinx.parcelize.Parcelize
|
||||
@Parcelize
|
||||
data class VkVideo(
|
||||
val id: Int,
|
||||
val ownerId: Int,
|
||||
val images: List<BaseVkVideo.Image>,
|
||||
val firstFrames: List<BaseVkVideo.FirstFrame>?
|
||||
val firstFrames: List<BaseVkVideo.FirstFrame>?,
|
||||
val accessKey: String?
|
||||
) : VkAttachment() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
@@ -18,4 +21,12 @@ data class VkVideo(
|
||||
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
|
||||
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() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val className: String = this::class.java.name
|
||||
|
||||
}
|
||||
@@ -40,7 +40,8 @@ data class BaseVkConversation(
|
||||
unreadCount = unread_count,
|
||||
membersCount = chat_settings?.members_count,
|
||||
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 {
|
||||
this.lastMessage = lastMessage
|
||||
this.pinnedMessage = chat_settings?.pinned_message?.asVkMessage()
|
||||
|
||||
@@ -23,7 +23,8 @@ data class BaseVkMessage(
|
||||
val payload: String,
|
||||
val geo: Geo?,
|
||||
val action: Action?,
|
||||
val ttl: Int
|
||||
val ttl: Int,
|
||||
val reply_message: BaseVkMessage?
|
||||
) : Parcelable {
|
||||
|
||||
fun asVkMessage() = VkMessage(
|
||||
@@ -44,6 +45,7 @@ data class BaseVkMessage(
|
||||
).also {
|
||||
it.attachments = VkUtils.parseAttachments(attachments)
|
||||
it.forwards = VkUtils.parseForwards(fwd_messages)
|
||||
it.replyMessage = VkUtils.parseReplyMessage(reply_message)
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
|
||||
@@ -13,7 +13,7 @@ data class BaseVkAudio(
|
||||
val url: String,
|
||||
val date: Int,
|
||||
val owner_id: Int,
|
||||
val access_key: String,
|
||||
val access_key: String?,
|
||||
val is_explicit: Boolean,
|
||||
val is_focus_track: Boolean,
|
||||
val is_licensed: Boolean,
|
||||
@@ -27,10 +27,12 @@ data class BaseVkAudio(
|
||||
|
||||
fun asVkAudio() = VkAudio(
|
||||
id = id,
|
||||
ownerId = owner_id,
|
||||
title = title,
|
||||
artist = artist,
|
||||
url = url,
|
||||
duration = duration
|
||||
duration = duration,
|
||||
accessKey = access_key
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkCall
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@@ -11,4 +12,15 @@ data class BaseVkCall(
|
||||
val time: Int,
|
||||
val duration: Int,
|
||||
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 preview: Preview?,
|
||||
val ic_licensed: Int,
|
||||
val access_key: String,
|
||||
val access_key: String?,
|
||||
val web_preview_url: String?
|
||||
) : BaseVkAttachment() {
|
||||
|
||||
fun asVkFile() = VkFile(
|
||||
id = id,
|
||||
ownerId = owner_id,
|
||||
title = title,
|
||||
ext = ext,
|
||||
url = url,
|
||||
size = size
|
||||
size = size,
|
||||
accessKey = access_key
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkGift
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@@ -9,4 +10,13 @@ data class BaseVkGift(
|
||||
val thumb_256: String?,
|
||||
val thumb_96: 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
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkGraffiti
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@@ -11,4 +12,15 @@ data class BaseVkGraffiti(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
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 caption: String?,
|
||||
val photo: BaseVkPhoto?,
|
||||
val target: String,
|
||||
val target: String?,
|
||||
val is_favorite: Boolean
|
||||
) : BaseVkAttachment() {
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ data class BaseVkPhoto(
|
||||
val has_tags: Boolean,
|
||||
val access_key: String?,
|
||||
val sizes: List<Size>,
|
||||
val text: String,
|
||||
val text: String?,
|
||||
val user_id: Int?,
|
||||
val lat: Double?,
|
||||
val long: Double?,
|
||||
|
||||
@@ -26,7 +26,7 @@ data class BaseVkVideo(
|
||||
val can_add_to_faves: Int,
|
||||
val can_add: Int,
|
||||
val can_attach_link: Int,
|
||||
val access_key: String,
|
||||
val access_key: String?,
|
||||
val owner_id: Int,
|
||||
val ov_id: String,
|
||||
val is_favorite: Boolean,
|
||||
@@ -40,8 +40,10 @@ data class BaseVkVideo(
|
||||
|
||||
fun asVkVideo() = VkVideo(
|
||||
id = id,
|
||||
ownerId = owner_id,
|
||||
images = image,
|
||||
firstFrames = first_frame
|
||||
firstFrames = first_frame,
|
||||
accessKey = access_key
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
|
||||
+16
-1
@@ -1,6 +1,7 @@
|
||||
package com.meloda.fast.api.model.base.attachments
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.attachments.VkVoiceMessage
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@@ -14,4 +15,18 @@ data class BaseVkVoiceMessage(
|
||||
val access_key: String,
|
||||
val transcript_state: 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"))
|
||||
}
|
||||
|
||||
// TODO: 9/29/2021 crash on timeout
|
||||
return chain.proceed(chain.request().newBuilder().apply { url(builder.build()) }.build())
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.meloda.fast.api.network
|
||||
|
||||
import com.meloda.fast.api.VKException
|
||||
import com.meloda.fast.api.base.ApiError
|
||||
import com.meloda.fast.api.base.ApiResponse
|
||||
import okhttp3.Request
|
||||
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
|
||||
|
||||
|
||||
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 {
|
||||
if (result.throwable is ApiError) {
|
||||
onFailure(call, result.throwable)
|
||||
return true
|
||||
}
|
||||
|
||||
val json = JSONObject(result.throwable.message ?: "{}")
|
||||
|
||||
return if (json.has("error")) {
|
||||
|
||||
+9
-1
@@ -1,6 +1,6 @@
|
||||
package com.meloda.fast.api.network
|
||||
|
||||
object VKUrls {
|
||||
object VkUrls {
|
||||
|
||||
const val OAUTH = "https://oauth.vk.com"
|
||||
const val API = "https://api.vk.com/method"
|
||||
@@ -12,6 +12,10 @@ object VKUrls {
|
||||
|
||||
object Conversations {
|
||||
const val Get = "$API/messages.getConversations"
|
||||
const val Delete = "$API/messages.deleteConversation"
|
||||
const val Pin = "$API/messages.pinConversation"
|
||||
const val Unpin = "$API/messages.unpinConversation"
|
||||
const val ReorderPinned = "$API/messages.reorderPinnedConversations"
|
||||
}
|
||||
|
||||
object Users {
|
||||
@@ -24,6 +28,10 @@ object VKUrls {
|
||||
const val MarkAsImportant = "$API/messages.markAsImportant"
|
||||
const val GetLongPollServer = "$API/messages.getLongPollServer"
|
||||
const val GetLongPollHistory = "$API/messages.getLongPollHistory"
|
||||
const val Pin = "$API/messages.pin"
|
||||
const val Unpin = "$API/messages.unpin"
|
||||
const val Delete = "$API/messages.delete"
|
||||
const val Edit = "$API/messages.edit"
|
||||
}
|
||||
|
||||
|
||||
+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
|
||||
|
||||
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 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 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 kotlinx.parcelize.Parcelize
|
||||
@@ -23,4 +23,19 @@ data class ConversationsGetRequest(
|
||||
extended?.let { this["extended"] = 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 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.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.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 javax.inject.Inject
|
||||
|
||||
@@ -26,8 +21,20 @@ class MessagesDataSource @Inject constructor(
|
||||
suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) =
|
||||
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 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.network.repo.UsersRepo
|
||||
import com.meloda.fast.api.model.request.UsersGetRequest
|
||||
import com.meloda.fast.database.dao.UsersDao
|
||||
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.model.base.BaseVkUser
|
||||
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.FormUrlEncoded
|
||||
import retrofit2.http.POST
|
||||
@@ -11,7 +11,7 @@ import retrofit2.http.POST
|
||||
interface UsersRepo {
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(VKUrls.Users.GetById)
|
||||
@POST(VkUrls.Users.GetById)
|
||||
suspend fun getById(
|
||||
@FieldMap params: Map<String, String>?
|
||||
): 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 kotlinx.parcelize.Parcelize
|
||||
@@ -0,0 +1,2 @@
|
||||
package com.meloda.fast.api.network.users
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package com.meloda.fast.base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity, LifecycleOwner {
|
||||
|
||||
@@ -39,10 +37,4 @@ abstract class BaseActivity : AppCompatActivity, LifecycleOwner {
|
||||
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.base.viewmodel.BaseViewModel
|
||||
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.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) {
|
||||
Toast.makeText(
|
||||
requireContext(), R.string.authorization_failed, Toast.LENGTH_LONG
|
||||
|
||||
@@ -114,7 +114,7 @@ abstract class BaseAdapter<Item, VH : BaseHolder>(
|
||||
holder.bind(position)
|
||||
}
|
||||
|
||||
protected fun initListeners(itemView: View, position: Int) {
|
||||
protected open fun initListeners(itemView: View, position: Int) {
|
||||
if (itemView is AdapterView<*>) return
|
||||
|
||||
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 = ""
|
||||
|
||||
protected val tasksEventChannel = Channel<VKEvent>()
|
||||
protected val tasksEventChannel = Channel<VkEvent>()
|
||||
val tasksEvent = tasksEventChannel.receiveAsFlow()
|
||||
|
||||
protected fun <T> makeJob(
|
||||
@@ -25,23 +25,35 @@ abstract class BaseViewModel : ViewModel() {
|
||||
onEnd: (suspend () -> Unit)? = null,
|
||||
onError: (suspend (Throwable) -> Unit)? = null
|
||||
) = viewModelScope.launch {
|
||||
onStart?.invoke()
|
||||
onStart?.invoke() ?: onStart()
|
||||
when (val response = job()) {
|
||||
is Answer.Success -> onAnswer(response.data)
|
||||
is Answer.Error -> {
|
||||
checkErrors(response.throwable)
|
||||
onError?.invoke(response.throwable)
|
||||
?: sendEvent(
|
||||
ErrorEvent(
|
||||
response.throwable.message
|
||||
?: unknownErrorDefaultText
|
||||
)
|
||||
)
|
||||
onError?.invoke(response.throwable) ?: onError(response.throwable)
|
||||
}
|
||||
}
|
||||
}.also { it.invokeOnCompletion { viewModelScope.launch { onEnd?.invoke() } } }
|
||||
}.also {
|
||||
it.invokeOnCompletion {
|
||||
viewModelScope.launch {
|
||||
onEnd?.invoke() ?: onStop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
when (throwable) {
|
||||
|
||||
@@ -5,15 +5,15 @@ data class ShowDialogInfoEvent(
|
||||
val message: String,
|
||||
val positiveBtn: 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()
|
||||
data class CaptchaEvent(val sid: String, val image: String) : VKEvent()
|
||||
data class ValidationEvent(val sid: String) : VKEvent()
|
||||
object IllegalTokenEvent : VkEvent()
|
||||
data class CaptchaEvent(val sid: String, val image: String) : VkEvent()
|
||||
data class ValidationEvent(val sid: String) : VkEvent()
|
||||
|
||||
object StartProgressEvent : VKEvent()
|
||||
object StopProgressEvent : VKEvent()
|
||||
object StartProgressEvent : VkEvent()
|
||||
object StopProgressEvent : VkEvent()
|
||||
|
||||
abstract class VKEvent
|
||||
abstract class VkEvent
|
||||
@@ -13,6 +13,7 @@ import kotlinx.coroutines.Job
|
||||
object AppSettings {
|
||||
|
||||
val keyIsMultilineEnabled = booleanPreferencesKey("isMultilineEnabled")
|
||||
|
||||
}
|
||||
|
||||
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
|
||||
|
||||
@@ -19,7 +19,7 @@ import com.meloda.fast.database.dao.UsersDao
|
||||
VkUser::class,
|
||||
VkGroup::class
|
||||
],
|
||||
version = 24,
|
||||
version = 26,
|
||||
exportSchema = false,
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
||||
@@ -4,11 +4,15 @@ import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.meloda.fast.api.network.AuthInterceptor
|
||||
import com.meloda.fast.api.network.ResultCallFactory
|
||||
import com.meloda.fast.api.network.datasource.AuthDataSource
|
||||
import com.meloda.fast.api.network.datasource.ConversationsDataSource
|
||||
import com.meloda.fast.api.network.datasource.MessagesDataSource
|
||||
import com.meloda.fast.api.network.datasource.UsersDataSource
|
||||
import com.meloda.fast.api.network.repo.*
|
||||
import com.meloda.fast.api.network.auth.AuthRepo
|
||||
import com.meloda.fast.api.network.auth.AuthDataSource
|
||||
import com.meloda.fast.api.network.conversations.ConversationsDataSource
|
||||
import com.meloda.fast.api.network.conversations.ConversationsRepo
|
||||
import com.meloda.fast.api.network.longpoll.LongPollRepo
|
||||
import com.meloda.fast.api.network.messages.MessagesDataSource
|
||||
import com.meloda.fast.api.network.users.UsersDataSource
|
||||
import com.meloda.fast.api.network.messages.MessagesRepo
|
||||
import com.meloda.fast.api.network.users.UsersRepo
|
||||
import com.meloda.fast.database.dao.ConversationsDao
|
||||
import com.meloda.fast.database.dao.MessagesDao
|
||||
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
|
||||
|
||||
import android.widget.TextView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
|
||||
object TextViewExtensions {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
val chatUser: VkUser? = if (conversation.isUser()) {
|
||||
profiles[conversation.id]
|
||||
} else null
|
||||
val conversationUser = VkUtils.getConversationUser(conversation, profiles)
|
||||
val conversationGroup = VkUtils.getConversationGroup(conversation, groups)
|
||||
|
||||
val messageUser: VkUser? = if (message.isUser()) {
|
||||
profiles[message.fromId]
|
||||
} else null
|
||||
val messageUser = VkUtils.getMessageUser(message, profiles)
|
||||
val messageGroup = VkUtils.getMessageGroup(message, groups)
|
||||
|
||||
val chatGroup: VkGroup? = if (conversation.isGroup()) {
|
||||
groups[conversation.id]
|
||||
} else null
|
||||
|
||||
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
|
||||
}
|
||||
val avatar = VkUtils.getConversationAvatar(
|
||||
conversation = conversation,
|
||||
conversationUser = conversationUser,
|
||||
conversationGroup = conversationGroup
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -210,7 +198,8 @@ class ConversationsAdapter constructor(
|
||||
binding.message.text = spanMessage
|
||||
|
||||
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)
|
||||
|
||||
@@ -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 {
|
||||
private val COMPARATOR = object : DiffUtil.ItemCallback<VkConversation>() {
|
||||
override fun areItemsTheSame(
|
||||
|
||||
+179
-36
@@ -1,43 +1,46 @@
|
||||
package com.meloda.fast.screens.conversations
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.viewbinding.library.fragment.viewBinding
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import coil.load
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.activity.MainActivity
|
||||
import com.meloda.fast.api.UserConfig
|
||||
import com.meloda.fast.api.model.VkConversation
|
||||
import com.meloda.fast.base.BaseViewModelFragment
|
||||
import com.meloda.fast.base.viewmodel.StartProgressEvent
|
||||
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.dataStore
|
||||
import com.meloda.fast.databinding.FragmentConversationsBinding
|
||||
import com.meloda.fast.util.AndroidUtils
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ConversationsFragment :
|
||||
BaseViewModelFragment<ConversationsViewModel>(R.layout.fragment_conversations) {
|
||||
|
||||
companion object {
|
||||
const val TAG = "ConversationsFragment"
|
||||
}
|
||||
|
||||
override val viewModel: ConversationsViewModel by viewModels()
|
||||
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 isExpanded = true
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
@@ -71,14 +91,10 @@ class ConversationsFragment :
|
||||
requireContext().dataStore.data.map {
|
||||
adapter.isMultilineEnabled = it[AppSettings.keyIsMultilineEnabled] ?: true
|
||||
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||
}.collect { }
|
||||
}.collect()
|
||||
}
|
||||
|
||||
binding.createChat.setOnClickListener {
|
||||
Snackbar.make(it, "Test snackbar", Snackbar.LENGTH_SHORT)
|
||||
.setAction("Action") {}
|
||||
.show()
|
||||
}
|
||||
binding.createChat.setOnClickListener {}
|
||||
|
||||
UserConfig.vkUser.observe(viewLifecycleOwner) {
|
||||
it?.let { user -> binding.avatar.load(user.photo200) { crossfade(100) } }
|
||||
@@ -87,28 +103,32 @@ class ConversationsFragment :
|
||||
binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
|
||||
if (isPaused) return@OnOffsetChangedListener
|
||||
|
||||
if (verticalOffset <= -100) {
|
||||
binding.avatarContainer.alpha = 0f
|
||||
return@OnOffsetChangedListener
|
||||
}
|
||||
binding.appBar.animate().translationZ(
|
||||
if (verticalOffset < 0) AndroidUtils.px(3).roundToInt().toFloat()
|
||||
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
|
||||
})
|
||||
|
||||
if (isPaused) {
|
||||
isPaused = false
|
||||
return
|
||||
}
|
||||
binding.avatar.setOnClickListener { avatarPopupMenu.show() }
|
||||
|
||||
binding.toolbar.overflowIcon = ContextCompat.getDrawable(requireContext(), R.drawable.test)
|
||||
|
||||
viewModel.loadProfileUser()
|
||||
viewModel.loadConversations()
|
||||
|
||||
binding.avatar.setOnClickListener {
|
||||
lifecycleScope.launchWhenResumed {
|
||||
binding.avatar.setOnLongClickListener {
|
||||
lifecycleScope.launch {
|
||||
requireContext().dataStore.edit { settings ->
|
||||
val isMultilineEnabled = settings[AppSettings.keyIsMultilineEnabled] ?: true
|
||||
settings[AppSettings.keyIsMultilineEnabled] = !isMultilineEnabled
|
||||
@@ -117,15 +137,59 @@ class ConversationsFragment :
|
||||
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
if (isPaused) {
|
||||
isPaused = false
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.loadProfileUser()
|
||||
viewModel.loadConversations()
|
||||
}
|
||||
|
||||
override fun onEvent(event: VKEvent) {
|
||||
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)
|
||||
when (event) {
|
||||
is ConversationsLoaded -> refreshConversations(event)
|
||||
is StartProgressEvent -> onProgressStarted()
|
||||
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>) {
|
||||
adapter.values.clear()
|
||||
adapter.values += values
|
||||
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||
adapter.submitList(values)
|
||||
}
|
||||
|
||||
private fun onItemClick(position: Int) {
|
||||
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(
|
||||
R.id.toMessagesHistory,
|
||||
@@ -198,8 +268,81 @@ class ConversationsFragment :
|
||||
}
|
||||
|
||||
private fun onItemLongClick(position: Int): Boolean {
|
||||
binding.createChat.performClick()
|
||||
showOptionsDialog(position)
|
||||
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 com.meloda.fast.api.UserConfig
|
||||
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.VkGroup
|
||||
import com.meloda.fast.api.model.VkUser
|
||||
import com.meloda.fast.api.model.request.ConversationsGetRequest
|
||||
import com.meloda.fast.api.model.request.UsersGetRequest
|
||||
import com.meloda.fast.api.network.conversations.*
|
||||
import com.meloda.fast.api.network.users.UsersDataSource
|
||||
import com.meloda.fast.api.network.users.UsersGetRequest
|
||||
import com.meloda.fast.base.viewmodel.BaseViewModel
|
||||
import com.meloda.fast.base.viewmodel.StartProgressEvent
|
||||
import com.meloda.fast.base.viewmodel.StopProgressEvent
|
||||
import com.meloda.fast.base.viewmodel.VKEvent
|
||||
import com.meloda.fast.base.viewmodel.VkEvent
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -22,18 +19,20 @@ import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ConversationsViewModel @Inject constructor(
|
||||
private val dataSource: ConversationsDataSource,
|
||||
private val usersDataSource: UsersDataSource
|
||||
private val conversations: ConversationsDataSource,
|
||||
private val users: UsersDataSource
|
||||
) : BaseViewModel() {
|
||||
|
||||
fun loadConversations() = viewModelScope.launch(Dispatchers.Default) {
|
||||
fun loadConversations(
|
||||
offset: Int? = null
|
||||
) = viewModelScope.launch(Dispatchers.Default) {
|
||||
makeJob({
|
||||
dataSource.getAllChats(
|
||||
conversations.get(
|
||||
ConversationsGetRequest(
|
||||
count = 30,
|
||||
// offset = 177,
|
||||
extended = true,
|
||||
fields = "${VKConstants.USER_FIELDS},${VKConstants.GROUP_FIELDS}"
|
||||
offset = offset,
|
||||
fields = VKConstants.ALL_FIELDS
|
||||
)
|
||||
)
|
||||
},
|
||||
@@ -52,6 +51,7 @@ class ConversationsViewModel @Inject constructor(
|
||||
sendEvent(
|
||||
ConversationsLoaded(
|
||||
count = response.count,
|
||||
offset = offset,
|
||||
unreadCount = response.unreadCount ?: 0,
|
||||
conversations = response.items.map { items ->
|
||||
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 {
|
||||
makeJob({
|
||||
usersDataSource.getById(UsersGetRequest(fields = "online,photo_200"))
|
||||
},
|
||||
makeJob({ users.getById(UsersGetRequest(fields = VKConstants.USER_FIELDS)) },
|
||||
onAnswer = {
|
||||
it.response?.let { r ->
|
||||
val users = r.map { u -> u.asVkUser() }
|
||||
usersDataSource.storeUsers(users)
|
||||
this@ConversationsViewModel.users.storeUsers(users)
|
||||
|
||||
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(
|
||||
val count: Int,
|
||||
val offset: Int?,
|
||||
val unreadCount: Int?,
|
||||
val conversations: List<VkConversation>,
|
||||
val profiles: HashMap<Int, VkUser>,
|
||||
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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.viewbinding.library.fragment.viewBinding
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
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.meloda.fast.BuildConfig
|
||||
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.viewmodel.*
|
||||
import com.meloda.fast.databinding.DialogCaptchaBinding
|
||||
@@ -29,7 +37,10 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jsoup.Jsoup
|
||||
import java.net.URLEncoder
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -59,14 +70,14 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
||||
binding.loginInput.clearFocus()
|
||||
}
|
||||
|
||||
override fun onEvent(event: VKEvent) {
|
||||
override fun onEvent(event: VkEvent) {
|
||||
super.onEvent(event)
|
||||
|
||||
when (event) {
|
||||
is ShowError -> showErrorSnackbar(event.errorDescription)
|
||||
is ErrorEvent -> showErrorSnackbar(event.errorText)
|
||||
is CaptchaEvent -> showCaptchaDialog(event.sid, event.image)
|
||||
is ValidationEvent -> showValidationRequired(event.sid)
|
||||
is SuccessAuth -> goToMain(event.haveAuthorized)
|
||||
is SuccessAuth -> goToMain(event)
|
||||
|
||||
is CodeSent -> showValidationDialog()
|
||||
is StartProgressEvent -> onProgressStarted()
|
||||
@@ -89,11 +100,91 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
||||
}
|
||||
|
||||
private fun prepareViews() {
|
||||
prepareWebView()
|
||||
prepareEmailEditText()
|
||||
preparePasswordEditText()
|
||||
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() {
|
||||
binding.loginInput.addTextChangedListener {
|
||||
if (!binding.loginLayout.error.isNullOrBlank()) binding.loginLayout.error = ""
|
||||
@@ -293,8 +384,13 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
||||
snackbar.show()
|
||||
}
|
||||
|
||||
private fun goToMain(haveAuthorized: Boolean) = lifecycleScope.launch {
|
||||
if (haveAuthorized) delay(500)
|
||||
private fun goToMain(event: SuccessAuth) = lifecycleScope.launch {
|
||||
UserConfig.userId = event.userId
|
||||
UserConfig.accessToken = event.vkToken
|
||||
|
||||
if (event.haveAuthorized) delay(500)
|
||||
|
||||
launchWebView()
|
||||
|
||||
findNavController().navigate(R.id.toMain)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package com.meloda.fast.screens.login
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.meloda.fast.api.UserConfig
|
||||
import com.meloda.fast.api.VKConstants
|
||||
import com.meloda.fast.api.VKException
|
||||
import com.meloda.fast.api.model.request.RequestAuthDirect
|
||||
import com.meloda.fast.api.network.datasource.AuthDataSource
|
||||
import com.meloda.fast.api.network.auth.AuthDataSource
|
||||
import com.meloda.fast.api.network.auth.RequestAuthDirect
|
||||
import com.meloda.fast.base.viewmodel.*
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -45,33 +44,37 @@ class LoginViewModel @Inject constructor(
|
||||
return@makeJob
|
||||
}
|
||||
|
||||
UserConfig.userId = it.userId
|
||||
UserConfig.accessToken = it.accessToken
|
||||
|
||||
sendEvent(SuccessAuth())
|
||||
sendEvent(
|
||||
SuccessAuth(
|
||||
userId = it.userId,
|
||||
vkToken = it.accessToken
|
||||
)
|
||||
)
|
||||
},
|
||||
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) }
|
||||
},
|
||||
onStart = { sendEvent(StartProgressEvent) },
|
||||
onEnd = { sendEvent(StopProgressEvent) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun sendSms(validationSid: String) = viewModelScope.launch {
|
||||
makeJob({ dataSource.sendSms(validationSid) },
|
||||
onAnswer = { sendEvent(CodeSent) },
|
||||
onError = {},
|
||||
onStart = {},
|
||||
onEnd = {})
|
||||
onAnswer = { sendEvent(CodeSent) }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class ShowError(val errorDescription: String) : VKEvent()
|
||||
object CodeSent : VkEvent()
|
||||
|
||||
object CodeSent : VKEvent()
|
||||
|
||||
data class SuccessAuth(val haveAuthorized: Boolean = true) : VKEvent()
|
||||
data class SuccessAuth(
|
||||
val haveAuthorized: Boolean = true,
|
||||
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.isVisible
|
||||
import androidx.core.view.setPadding
|
||||
import androidx.core.view.updatePadding
|
||||
import coil.load
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.api.UserConfig
|
||||
import com.meloda.fast.api.VkUtils
|
||||
import com.meloda.fast.api.model.VkGroup
|
||||
import com.meloda.fast.api.model.VkMessage
|
||||
@@ -30,9 +32,11 @@ import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
// TODO: 9/29/2021 use recyclerview for viewing attachments
|
||||
class AttachmentInflater constructor(
|
||||
private val context: Context,
|
||||
private val container: LinearLayoutCompat,
|
||||
private val textContainer: LinearLayoutCompat,
|
||||
private val message: VkMessage,
|
||||
private val profiles: Map<Int, VkUser>,
|
||||
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 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() {
|
||||
if (message.attachments.isNullOrEmpty()) return
|
||||
attachments = message.attachments!!
|
||||
|
||||
container.removeAllViews()
|
||||
textContainer.removeAllViews()
|
||||
|
||||
if (attachments.size == 1) {
|
||||
when (val attachment = attachments[0]) {
|
||||
is VkSticker -> return sticker(attachment)
|
||||
is VkWall -> return wall(attachment)
|
||||
is VkVoiceMessage -> return voice(attachment)
|
||||
is VkCall -> return call(attachment)
|
||||
is VkGraffiti -> return graffiti(attachment)
|
||||
is VkGift -> return gift(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +98,6 @@ class AttachmentInflater constructor(
|
||||
is VkAudio -> audio(attachment)
|
||||
is VkFile -> file(attachment)
|
||||
is VkLink -> link(attachment)
|
||||
is VkStory -> story(attachment)
|
||||
|
||||
else -> Log.e(
|
||||
"Attachment inflater",
|
||||
@@ -94,12 +109,15 @@ class AttachmentInflater constructor(
|
||||
}
|
||||
|
||||
private fun photo(photo: VkPhoto) {
|
||||
val size = photo.sizeOfType('m') ?: return
|
||||
val size = photo.getSizeOrSmaller('y') ?: return
|
||||
|
||||
val newPhoto = ShapeableImageView(context).apply {
|
||||
layoutParams = LinearLayoutCompat.LayoutParams(
|
||||
AndroidUtils.px(size.width).roundToInt(),
|
||||
AndroidUtils.px(size.height).roundToInt()
|
||||
// ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
size.width,
|
||||
size.height
|
||||
// AndroidUtils.px(size.width).roundToInt(),
|
||||
// AndroidUtils.px(size.height).roundToInt()
|
||||
)
|
||||
|
||||
shapeAppearanceModel =
|
||||
@@ -110,6 +128,12 @@ class AttachmentInflater constructor(
|
||||
scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
}
|
||||
|
||||
if (photoClickListener != null) {
|
||||
newPhoto.setOnClickListener { photoClickListener?.invoke(size.url) }
|
||||
} else {
|
||||
newPhoto.setOnClickListener(null)
|
||||
}
|
||||
|
||||
val spacer = Space(context).also {
|
||||
it.layoutParams = LinearLayoutCompat.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
@@ -222,14 +246,7 @@ class AttachmentInflater constructor(
|
||||
binding.caption.text = link.caption
|
||||
binding.caption.isVisible = !link.caption.isNullOrBlank()
|
||||
|
||||
binding.preview.shapeAppearanceModel.toBuilder()
|
||||
.setAllCornerSizes(40f)
|
||||
.build()
|
||||
.let {
|
||||
binding.preview.shapeAppearanceModel = it
|
||||
}
|
||||
|
||||
link.photo?.sizeOfType('m')?.let {
|
||||
link.photo?.getMaxSize()?.let {
|
||||
binding.preview.load(it.url) { crossfade(150) }
|
||||
binding.preview.isVisible = true
|
||||
return
|
||||
@@ -245,8 +262,8 @@ class AttachmentInflater constructor(
|
||||
|
||||
with(binding.image) {
|
||||
layoutParams = LinearLayoutCompat.LayoutParams(
|
||||
AndroidUtils.px(180).roundToInt(),
|
||||
AndroidUtils.px(180).roundToInt()
|
||||
AndroidUtils.px(140).roundToInt(),
|
||||
AndroidUtils.px(140).roundToInt()
|
||||
)
|
||||
|
||||
load(url) { crossfade(150) }
|
||||
@@ -282,7 +299,7 @@ class AttachmentInflater constructor(
|
||||
|
||||
binding.avatar.isVisible = group != null || user != null
|
||||
binding.avatar.shapeAppearanceModel.toBuilder()
|
||||
.setAllCornerSizes(40f)
|
||||
.setAllCornerSizes(AndroidUtils.px(20))
|
||||
.build()
|
||||
.let {
|
||||
binding.avatar.shapeAppearanceModel = it
|
||||
@@ -302,8 +319,91 @@ class AttachmentInflater constructor(
|
||||
).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.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.LinearLayoutCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
@@ -29,7 +30,9 @@ class MessagesHistoryAdapter constructor(
|
||||
val conversation: VkConversation,
|
||||
val profiles: HashMap<Int, VkUser> = 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 {
|
||||
when {
|
||||
@@ -49,7 +52,7 @@ class MessagesHistoryAdapter constructor(
|
||||
private fun isPositionHeader(position: Int) = position == 0
|
||||
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) {
|
||||
// magick numbers is great!
|
||||
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 {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
@@ -78,22 +96,22 @@ class MessagesHistoryAdapter constructor(
|
||||
isFocusable = false
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||
override fun onBindViewHolder(holder: BasicHolder, position: Int) {
|
||||
if (holder is Header || holder is Footer) return
|
||||
|
||||
initListeners(holder.itemView, position)
|
||||
holder.bind(position)
|
||||
}
|
||||
|
||||
open inner class 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(
|
||||
private val binding: ItemMessageInBinding
|
||||
) : Holder(binding.root) {
|
||||
) : BasicHolder(binding.root) {
|
||||
|
||||
override fun bind(position: Int) {
|
||||
val message = getItem(position)
|
||||
@@ -103,33 +121,42 @@ class MessagesHistoryAdapter constructor(
|
||||
|
||||
MessagesPreparator(
|
||||
context = context,
|
||||
|
||||
root = binding.root,
|
||||
|
||||
conversation = conversation,
|
||||
message = message,
|
||||
prevMessage = prevMessage,
|
||||
nextMessage = nextMessage,
|
||||
|
||||
title = binding.title,
|
||||
|
||||
avatar = binding.avatar,
|
||||
bubble = binding.bubble,
|
||||
text = binding.text,
|
||||
spacer = binding.spacer,
|
||||
time = binding.time,
|
||||
unread = binding.unread,
|
||||
|
||||
textContainer = binding.textContainer,
|
||||
attachmentContainer = binding.attachmentContainer,
|
||||
attachmentSpacer = binding.attachmentSpacer,
|
||||
|
||||
profiles = profiles,
|
||||
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(
|
||||
private val binding: ItemMessageOutBinding
|
||||
) : Holder(binding.root) {
|
||||
|
||||
init {
|
||||
binding.bubbleStroke.setOnClickListener { binding.bubble.performClick() }
|
||||
}
|
||||
) : BasicHolder(binding.root) {
|
||||
|
||||
override fun bind(position: Int) {
|
||||
val message = getItem(position)
|
||||
@@ -138,16 +165,17 @@ class MessagesHistoryAdapter constructor(
|
||||
|
||||
MessagesPreparator(
|
||||
context = context,
|
||||
root = binding.root,
|
||||
conversation = conversation,
|
||||
message = message,
|
||||
prevMessage = prevMessage,
|
||||
|
||||
bubble = binding.bubble,
|
||||
bubbleStroke = binding.bubbleStroke,
|
||||
text = binding.text,
|
||||
spacer = binding.spacer,
|
||||
time = binding.time,
|
||||
unread = binding.unread,
|
||||
|
||||
textContainer = binding.textContainer,
|
||||
attachmentContainer = binding.attachmentContainer,
|
||||
attachmentSpacer = binding.attachmentSpacer,
|
||||
|
||||
@@ -159,7 +187,7 @@ class MessagesHistoryAdapter constructor(
|
||||
|
||||
inner class ServiceMessage(
|
||||
private val binding: ItemMessageServiceBinding
|
||||
) : Holder(binding.root) {
|
||||
) : BasicHolder(binding.root) {
|
||||
|
||||
private val youPrefix = context.getString(R.string.you_message_prefix)
|
||||
|
||||
@@ -198,7 +226,7 @@ class MessagesHistoryAdapter constructor(
|
||||
|
||||
binding.photo.isVisible = true
|
||||
|
||||
val size = attachment.sizeOfType('m') ?: return@let
|
||||
val size = attachment.getSizeOrSmaller('y') ?: return@let
|
||||
|
||||
binding.photo.layoutParams = LinearLayoutCompat.LayoutParams(
|
||||
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 {
|
||||
if (actualSize == 0) return 2
|
||||
return super.getItemCount() + 2
|
||||
return null
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package com.meloda.fast.screens.messages
|
||||
|
||||
import android.graphics.Color
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
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.setPadding
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
@@ -16,6 +20,8 @@ import coil.load
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.api.UserConfig
|
||||
import com.meloda.fast.api.VKConstants
|
||||
import com.meloda.fast.api.VkUtils
|
||||
import com.meloda.fast.api.model.VkConversation
|
||||
import com.meloda.fast.api.model.VkGroup
|
||||
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.viewmodel.StartProgressEvent
|
||||
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.extensions.TextViewExtensions.clear
|
||||
import com.meloda.fast.util.AndroidUtils
|
||||
@@ -32,6 +39,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MessagesHistoryFragment :
|
||||
@@ -43,7 +51,7 @@ class MessagesHistoryFragment :
|
||||
private val action = MutableLiveData<Action>()
|
||||
|
||||
private enum class Action {
|
||||
RECORD, SEND
|
||||
RECORD, SEND, EDIT, DELETE
|
||||
}
|
||||
|
||||
private val user: VkUser? by lazy {
|
||||
@@ -62,14 +70,19 @@ class MessagesHistoryFragment :
|
||||
MessagesHistoryAdapter(requireContext(), mutableListOf(), conversation).also {
|
||||
it.itemClickListener = this::onItemClick
|
||||
it.itemLongClickListener = this::onItemLongClick
|
||||
it.avatarLongClickListener = this::onAvatarLongClickListener
|
||||
}
|
||||
}
|
||||
|
||||
private var timestampTimer: Timer? = null
|
||||
|
||||
private lateinit var attachmentController: AttachmentPanelController
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
attachmentController = AttachmentPanelController().init()
|
||||
|
||||
val title = when {
|
||||
conversation.isChat() -> conversation.title
|
||||
conversation.isUser() -> user?.toString()
|
||||
@@ -98,19 +111,7 @@ class MessagesHistoryFragment :
|
||||
|
||||
binding.status.text = status ?: "..."
|
||||
|
||||
val avatar = when {
|
||||
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
|
||||
prepareAvatar()
|
||||
|
||||
prepareViews()
|
||||
|
||||
@@ -166,8 +167,14 @@ class MessagesHistoryFragment :
|
||||
})
|
||||
|
||||
binding.message.doAfterTextChanged {
|
||||
val newValue = if (it.toString().isNotBlank()) Action.SEND
|
||||
else Action.RECORD
|
||||
val canSend = it.toString().isNotBlank()
|
||||
|
||||
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
|
||||
}
|
||||
@@ -192,52 +199,159 @@ class MessagesHistoryFragment :
|
||||
Action.SEND -> {
|
||||
binding.action.setImageResource(R.drawable.ic_round_send_24)
|
||||
}
|
||||
Action.EDIT -> {
|
||||
binding.action.setImageResource(R.drawable.ic_round_done_24)
|
||||
}
|
||||
Action.DELETE -> {
|
||||
binding.action.setImageResource(R.drawable.ic_trash_can_outline_24)
|
||||
}
|
||||
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() {
|
||||
when (action.value) {
|
||||
Action.RECORD -> {
|
||||
}
|
||||
Action.SEND -> {
|
||||
val messageText = binding.message.text.toString().trim()
|
||||
if (messageText.isBlank()) return
|
||||
|
||||
val date = System.currentTimeMillis()
|
||||
|
||||
val message = VkMessage(
|
||||
id = -1,
|
||||
text = messageText,
|
||||
isOut = true,
|
||||
peerId = conversation.id,
|
||||
fromId = UserConfig.userId,
|
||||
date = (date / 1000).toInt(),
|
||||
randomId = 0,
|
||||
replyMessage = attachmentController.message.value
|
||||
)
|
||||
|
||||
adapter.add(message)
|
||||
adapter.notifyItemInserted(adapter.actualSize - 1)
|
||||
binding.recyclerView.smoothScrollToPosition(adapter.lastPosition)
|
||||
binding.message.clear()
|
||||
|
||||
val replyMessage = attachmentController.message.value
|
||||
attachmentController.message.value = null
|
||||
|
||||
viewModel.sendMessage(
|
||||
peerId = conversation.id,
|
||||
message = messageText,
|
||||
randomId = 0,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun performAction() {
|
||||
if (action.value == Action.RECORD) {
|
||||
|
||||
} else if (action.value == Action.SEND) {
|
||||
val messageText = binding.message.text.toString().trim()
|
||||
if (messageText.isBlank()) return
|
||||
|
||||
val date = System.currentTimeMillis()
|
||||
|
||||
var message = VkMessage(
|
||||
id = -1,
|
||||
text = messageText,
|
||||
isOut = true,
|
||||
peerId = conversation.id,
|
||||
fromId = UserConfig.userId,
|
||||
date = (date / 1000).toInt(),
|
||||
randomId = 0
|
||||
)
|
||||
|
||||
adapter.add(message)
|
||||
adapter.notifyDataSetChanged()
|
||||
binding.recyclerView.smoothScrollToPosition(adapter.lastPosition)
|
||||
binding.message.clear()
|
||||
|
||||
viewModel.sendMessage(
|
||||
peerId = conversation.id,
|
||||
message = messageText,
|
||||
randomId = 0
|
||||
) { message = message.copyMessage(id = it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEvent(event: VKEvent) {
|
||||
override fun onEvent(event: VkEvent) {
|
||||
super.onEvent(event)
|
||||
|
||||
when (event) {
|
||||
is MessagesMarkAsImportant -> markMessagesAsImportant(event)
|
||||
is MessagesLoaded -> refreshMessages(event)
|
||||
is StartProgressEvent -> onProgressStarted()
|
||||
is StopProgressEvent -> onProgressStopped()
|
||||
|
||||
is MessagesMarkAsImportant -> markMessagesAsImportant(event)
|
||||
is MessagesLoaded -> refreshMessages(event)
|
||||
is MessagesPin -> conversation.pinnedMessage = event.message
|
||||
is MessagesUnpin -> conversation.pinnedMessage = null
|
||||
is MessagesDelete -> deleteMessages(event)
|
||||
is MessagesEdit -> editMessage(event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,17 +397,21 @@ class MessagesHistoryFragment :
|
||||
|
||||
private fun markMessagesAsImportant(event: MessagesMarkAsImportant) {
|
||||
var changed = false
|
||||
val positions = mutableListOf<Int>()
|
||||
|
||||
for (i in adapter.values.indices) {
|
||||
val message = adapter.values[i]
|
||||
message.important = event.important
|
||||
if (event.messagesIds.contains(message.id)) {
|
||||
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) {
|
||||
@@ -315,30 +433,235 @@ class MessagesHistoryFragment :
|
||||
}
|
||||
|
||||
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]
|
||||
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())
|
||||
.setItems(params) { _, which ->
|
||||
if (which == 0) {
|
||||
viewModel.markAsImportant(
|
||||
val reply = getString(R.string.message_context_action_reply)
|
||||
|
||||
val isMessageAlreadyPinned = message.id == conversation.pinnedMessage?.id
|
||||
|
||||
val pin = getString(
|
||||
if (isMessageAlreadyPinned) R.string.message_context_action_unpin
|
||||
else R.string.message_context_action_pin
|
||||
)
|
||||
|
||||
val 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),
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun editMessage(event: MessagesEdit) {
|
||||
adapter.searchMessageIndex(event.message.id)?.let { index ->
|
||||
adapter.values[index] = event.message
|
||||
adapter.notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
message.value = null
|
||||
return this
|
||||
}
|
||||
|
||||
}
|
||||
private fun applyMessage(message: VkMessage) {
|
||||
showPanel()
|
||||
|
||||
private fun onItemLongClick(position: Int): Boolean {
|
||||
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.VkMessage
|
||||
import com.meloda.fast.api.model.VkUser
|
||||
import com.meloda.fast.api.model.request.MessagesGetHistoryRequest
|
||||
import com.meloda.fast.api.model.request.MessagesMarkAsImportantRequest
|
||||
import com.meloda.fast.api.model.request.MessagesSendRequest
|
||||
import com.meloda.fast.api.network.datasource.MessagesDataSource
|
||||
import com.meloda.fast.api.model.attachments.VkAttachment
|
||||
import com.meloda.fast.api.network.messages.*
|
||||
import com.meloda.fast.base.viewmodel.BaseViewModel
|
||||
import com.meloda.fast.base.viewmodel.StartProgressEvent
|
||||
import com.meloda.fast.base.viewmodel.StopProgressEvent
|
||||
import com.meloda.fast.base.viewmodel.VKEvent
|
||||
import com.meloda.fast.base.viewmodel.VkEvent
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MessagesHistoryViewModel @Inject constructor(
|
||||
private val dataSource: MessagesDataSource
|
||||
private val messages: MessagesDataSource
|
||||
) : BaseViewModel() {
|
||||
|
||||
fun loadHistory(
|
||||
peerId: Int
|
||||
) = viewModelScope.launch {
|
||||
makeJob({
|
||||
dataSource.getHistory(
|
||||
messages.getHistory(
|
||||
MessagesGetHistoryRequest(
|
||||
count = 30,
|
||||
peerId = peerId,
|
||||
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 ->
|
||||
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>()
|
||||
response.conversations?.let { baseConversations ->
|
||||
baseConversations.forEach { baseConversation ->
|
||||
baseConversation.asVkConversation(
|
||||
messages[baseConversation.last_message_id]
|
||||
hashMessages[baseConversation.last_message_id]
|
||||
).let { conversation -> conversations[conversation.id] = conversation }
|
||||
}
|
||||
}
|
||||
@@ -75,41 +71,33 @@ class MessagesHistoryViewModel @Inject constructor(
|
||||
profiles = profiles,
|
||||
groups = groups,
|
||||
conversations = conversations,
|
||||
messages = messages.values.toList()
|
||||
messages = hashMessages.values.toList()
|
||||
)
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
val throwable = it
|
||||
throw it
|
||||
},
|
||||
onStart = { sendEvent(StartProgressEvent) },
|
||||
onEnd = { sendEvent(StopProgressEvent) })
|
||||
})
|
||||
}
|
||||
|
||||
fun sendMessage(
|
||||
peerId: Int,
|
||||
message: String? = null,
|
||||
randomId: Int = 0,
|
||||
replyTo: Int? = null,
|
||||
setId: ((messageId: Int) -> Unit)? = null
|
||||
) = viewModelScope.launch {
|
||||
makeJob(
|
||||
{
|
||||
dataSource.send(
|
||||
messages.send(
|
||||
MessagesSendRequest(
|
||||
peerId = peerId,
|
||||
randomId = randomId,
|
||||
message = message
|
||||
message = message,
|
||||
replyTo = replyTo
|
||||
)
|
||||
)
|
||||
},
|
||||
onAnswer = {
|
||||
val response = it.response ?: return@makeJob
|
||||
setId?.invoke(response)
|
||||
},
|
||||
onError = {
|
||||
val throwable = it
|
||||
val i = 0
|
||||
})
|
||||
}
|
||||
|
||||
@@ -118,7 +106,7 @@ class MessagesHistoryViewModel @Inject constructor(
|
||||
important: Boolean
|
||||
) = viewModelScope.launch {
|
||||
makeJob({
|
||||
dataSource.markAsImportant(
|
||||
messages.markAsImportant(
|
||||
MessagesMarkAsImportantRequest(
|
||||
messagesIds = messagesIds,
|
||||
important = important
|
||||
@@ -133,13 +121,84 @@ class MessagesHistoryViewModel @Inject constructor(
|
||||
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(
|
||||
@@ -148,9 +207,23 @@ data class MessagesLoaded(
|
||||
val messages: List<VkMessage>,
|
||||
val profiles: HashMap<Int, VkUser>,
|
||||
val groups: HashMap<Int, VkGroup>
|
||||
) : VKEvent()
|
||||
) : VkEvent()
|
||||
|
||||
data class MessagesMarkAsImportant(
|
||||
val messagesIds: List<Int>,
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
@@ -8,9 +9,7 @@ import android.widget.Space
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.LinearLayoutCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.setPadding
|
||||
import coil.load
|
||||
import com.meloda.fast.R
|
||||
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.attachments.VkSticker
|
||||
import com.meloda.fast.common.AppGlobal
|
||||
import com.meloda.fast.util.AndroidUtils
|
||||
import com.meloda.fast.widget.BoundedLinearLayout
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
@@ -30,19 +28,21 @@ import kotlin.math.roundToInt
|
||||
class MessagesPreparator constructor(
|
||||
private val context: Context,
|
||||
|
||||
private val root: View? = null,
|
||||
|
||||
private val conversation: VkConversation,
|
||||
private val message: VkMessage,
|
||||
private val prevMessage: VkMessage? = null,
|
||||
private val nextMessage: VkMessage? = null,
|
||||
|
||||
private val bubble: BoundedLinearLayout? = null,
|
||||
private val bubbleStroke: View? = null,
|
||||
private val text: TextView? = null,
|
||||
private val avatar: ImageView? = null,
|
||||
private val title: TextView? = null,
|
||||
private val spacer: Space? = null,
|
||||
private val unread: ImageView? = null,
|
||||
private val time: TextView? = null,
|
||||
private val textContainer: LinearLayoutCompat? = null,
|
||||
private val attachmentContainer: LinearLayoutCompat? = null,
|
||||
private val attachmentSpacer: Space? = null,
|
||||
|
||||
@@ -65,51 +65,129 @@ class MessagesPreparator constructor(
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background)
|
||||
private val backgroundMiddleOut =
|
||||
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 backgroundMiddleStrokeOut =
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle_stroke)
|
||||
|
||||
private val rootHighlightedColor =
|
||||
ContextCompat.getColor(context, R.color.n2_100)
|
||||
|
||||
private var photoClickListener: ((url: String) -> Unit)? = null
|
||||
|
||||
fun setPhotoClickListener(unit: ((url: String) -> Unit)?): MessagesPreparator {
|
||||
this.photoClickListener = unit
|
||||
return this
|
||||
}
|
||||
|
||||
fun prepare() {
|
||||
val messageUser: VkUser? = if (message.isUser()) {
|
||||
profiles[message.fromId]
|
||||
} else null
|
||||
val messageUser = VkUtils.getMessageUser(message, profiles)
|
||||
val messageGroup = VkUtils.getMessageGroup(message, groups)
|
||||
|
||||
val messageGroup: VkGroup? = if (message.isGroup()) {
|
||||
groups[message.fromId]
|
||||
} else null
|
||||
prepareRootBackground()
|
||||
|
||||
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) {
|
||||
unread.isVisible = message.isRead(conversation)
|
||||
}
|
||||
}
|
||||
|
||||
if (bubble != null && time != null) {
|
||||
bubble.setOnClickListener { time.isVisible = !time.isVisible }
|
||||
}
|
||||
private fun prepareSpacer() {
|
||||
spacer?.isVisible = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message)
|
||||
}
|
||||
|
||||
if (attachmentContainer != null) {
|
||||
private fun prepareAttachments() {
|
||||
if (attachmentContainer != null && textContainer != null) {
|
||||
if (message.attachments.isNullOrEmpty()) {
|
||||
attachmentContainer.isVisible = false
|
||||
attachmentContainer.removeAllViews()
|
||||
} else {
|
||||
attachmentContainer.isVisible = true
|
||||
|
||||
AttachmentInflater(
|
||||
context = context,
|
||||
container = attachmentContainer,
|
||||
textContainer = textContainer,
|
||||
message = message,
|
||||
groups = groups,
|
||||
profiles = profiles
|
||||
).inflate()
|
||||
)
|
||||
.setPhotoClickListener(photoClickListener)
|
||||
.inflate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareAttachmentsSpacer() {
|
||||
attachmentSpacer?.isVisible =
|
||||
!message.attachments.isNullOrEmpty() && text?.isVisible == true
|
||||
}
|
||||
|
||||
private fun prepareBubbleBackground() {
|
||||
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
|
||||
bubble.background =
|
||||
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 (message.text == null) {
|
||||
text.isVisible = false
|
||||
bubble.isVisible = !message.attachments.isNullOrEmpty()
|
||||
bubbleStroke?.isVisible = bubble.isVisible
|
||||
} else {
|
||||
text.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) {
|
||||
val avatarUrl = when {
|
||||
message.isUser() && messageUser != null && !messageUser.photo200.isNullOrBlank() -> messageUser.photo200
|
||||
message.isGroup() && messageGroup != null && !messageGroup.photo200.isNullOrBlank() -> messageGroup.photo200
|
||||
else -> null
|
||||
}
|
||||
val avatarUrl = VkUtils.getMessageAvatar(message, messageUser, messageGroup)
|
||||
|
||||
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
|
||||
|
||||
import android.util.Log
|
||||
import com.meloda.fast.api.model.request.MessagesGetLongPollServerRequest
|
||||
import com.meloda.fast.api.network.datasource.MessagesDataSource
|
||||
import com.meloda.fast.api.network.repo.LongPollRepo
|
||||
import com.meloda.fast.api.network.messages.MessagesGetLongPollServerRequest
|
||||
import com.meloda.fast.api.network.messages.MessagesDataSource
|
||||
import com.meloda.fast.api.network.longpoll.LongPollRepo
|
||||
import kotlinx.coroutines.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
@@ -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 {
|
||||
|
||||
const val ONE_DAY_IN_SECONDS = 86400
|
||||
|
||||
fun removeTime(date: Date): Long {
|
||||
return Calendar.getInstance().apply {
|
||||
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.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
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.util.AndroidUtils
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class NoItemsView @JvmOverloads constructor(
|
||||
@@ -44,7 +43,7 @@ class NoItemsView @JvmOverloads constructor(
|
||||
private fun create() {
|
||||
val a = context.obtainStyledAttributes(attrs, R.styleable.NoItemsView)
|
||||
|
||||
minimumWidth = AndroidUtils.px(256).int()
|
||||
minimumWidth = AndroidUtils.px(256).roundToInt()
|
||||
minimumHeight = minimumWidth
|
||||
|
||||
orientation = VERTICAL
|
||||
@@ -53,8 +52,8 @@ class NoItemsView @JvmOverloads constructor(
|
||||
noItemsPicture = ImageView(context)
|
||||
|
||||
val params = imageViewParams
|
||||
params.height = AndroidUtils.px(64).int()
|
||||
params.width = AndroidUtils.px(64).int()
|
||||
params.height = AndroidUtils.px(64).roundToInt()
|
||||
params.width = AndroidUtils.px(64).roundToInt()
|
||||
|
||||
noItemsPicture.layoutParams = params
|
||||
|
||||
@@ -73,10 +72,10 @@ class NoItemsView @JvmOverloads constructor(
|
||||
noItemsTextView = TextView(context)
|
||||
|
||||
val textParams = textViewParams
|
||||
textParams.width = AndroidUtils.px(256).int()
|
||||
textParams.width = AndroidUtils.px(256).roundToInt()
|
||||
|
||||
if (noItemsDrawable != null) {
|
||||
textParams.topMargin = AndroidUtils.px(8).int()
|
||||
textParams.topMargin = AndroidUtils.px(8).roundToInt()
|
||||
}
|
||||
|
||||
noItemsTextView.layoutParams = textParams
|
||||
@@ -103,7 +102,7 @@ class NoItemsView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
fun setNoItemsImage(@DrawableRes resId: Int) {
|
||||
setNoItemsImage(context.drawable(resId))
|
||||
setNoItemsImage(AppCompatResources.getDrawable(context, resId))
|
||||
}
|
||||
|
||||
fun setNoItemsImage(drawable: Drawable?) {
|
||||
@@ -111,7 +110,7 @@ class NoItemsView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
fun setNoItemsImageTint(@ColorInt color: Int) {
|
||||
noItemsPicture.drawable.tint(color)
|
||||
noItemsPicture.drawable?.setTint(color)
|
||||
}
|
||||
|
||||
fun setNoItemsText(@StringRes resId: Int) {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<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 xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
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>
|
||||
|
||||
@@ -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