illegal token checking
fixes
This commit is contained in:
@@ -79,6 +79,8 @@ dependencies {
|
|||||||
|
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
|
||||||
|
|
||||||
|
implementation("androidx.work:work-runtime-ktx:2.6.0")
|
||||||
|
|
||||||
implementation("androidx.appcompat:appcompat:1.4.0-alpha03")
|
implementation("androidx.appcompat:appcompat:1.4.0-alpha03")
|
||||||
implementation("com.google.android.material:material:1.5.0-alpha03")
|
implementation("com.google.android.material:material:1.5.0-alpha03")
|
||||||
implementation("androidx.core:core-ktx:1.7.0-alpha02")
|
implementation("androidx.core:core-ktx:1.7.0-alpha02")
|
||||||
|
|||||||
@@ -14,7 +14,8 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:testOnly="false"
|
android:testOnly="false"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme"
|
||||||
|
tools:replace="android:allowBackup">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activity.MainActivity"
|
android:name=".activity.MainActivity"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ object VKConstants {
|
|||||||
const val GROUP_FIELDS = "description,members_count,counters,status,verified"
|
const val GROUP_FIELDS = "description,members_count,counters,status,verified"
|
||||||
|
|
||||||
const val USER_FIELDS =
|
const val USER_FIELDS =
|
||||||
"photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex"
|
"photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex,online_info"
|
||||||
|
|
||||||
|
|
||||||
const val API_VERSION = "5.132"
|
const val API_VERSION = "5.132"
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ package com.meloda.fast.api
|
|||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class VKException(var url: String = "", var description: String = "", var error: String) :
|
open class VKException(
|
||||||
|
var url: String = "",
|
||||||
|
var code: Int = -1,
|
||||||
|
var description: String = "",
|
||||||
|
var error: String
|
||||||
|
) :
|
||||||
IOException(description) {
|
IOException(description) {
|
||||||
|
|
||||||
var captcha: Pair<String, String>? = null
|
var captcha: Pair<String, String>? = null
|
||||||
|
|||||||
@@ -13,18 +13,18 @@ import com.meloda.fast.api.model.VkUser
|
|||||||
import com.meloda.fast.api.model.attachments.*
|
import com.meloda.fast.api.model.attachments.*
|
||||||
import com.meloda.fast.api.model.base.BaseVkMessage
|
import com.meloda.fast.api.model.base.BaseVkMessage
|
||||||
import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
|
import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
|
||||||
import com.meloda.fast.api.network.VKErrors
|
import com.meloda.fast.api.network.VkErrors
|
||||||
|
|
||||||
object VkUtils {
|
object VkUtils {
|
||||||
|
|
||||||
fun isValidationRequired(throwable: Throwable): Boolean {
|
fun isValidationRequired(throwable: Throwable): Boolean {
|
||||||
if (throwable !is VKException) return false
|
if (throwable !is VKException) return false
|
||||||
return throwable.error == VKErrors.NEED_VALIDATION
|
return throwable.error == VkErrors.NEED_VALIDATION
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isCaptchaRequired(throwable: Throwable): Boolean {
|
fun isCaptchaRequired(throwable: Throwable): Boolean {
|
||||||
if (throwable !is VKException) return false
|
if (throwable !is VKException) return false
|
||||||
return throwable.error == VKErrors.NEED_CAPTCHA
|
return throwable.error == VkErrors.NEED_CAPTCHA
|
||||||
}
|
}
|
||||||
|
|
||||||
fun prepareMessageText(text: String): String {
|
fun prepareMessageText(text: String): String {
|
||||||
@@ -94,9 +94,7 @@ object VkUtils {
|
|||||||
}
|
}
|
||||||
BaseVkAttachmentItem.AttachmentType.STICKER -> {
|
BaseVkAttachmentItem.AttachmentType.STICKER -> {
|
||||||
val sticker = baseAttachment.sticker ?: continue
|
val sticker = baseAttachment.sticker ?: continue
|
||||||
attachments += VkSticker(
|
attachments += sticker.asVkSticker()
|
||||||
link = sticker.images[0].url
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
BaseVkAttachmentItem.AttachmentType.GIFT -> {
|
BaseVkAttachmentItem.AttachmentType.GIFT -> {
|
||||||
val gift = baseAttachment.gift ?: continue
|
val gift = baseAttachment.gift ?: continue
|
||||||
@@ -275,9 +273,9 @@ object VkUtils {
|
|||||||
else -> return null
|
else -> return null
|
||||||
} ?: return null
|
} ?: return null
|
||||||
|
|
||||||
val actionMessage = message.actionMessage ?: return null
|
val actionMessage = message.actionMessage
|
||||||
|
|
||||||
"$prefix pinned message «$actionMessage»"
|
"$prefix pinned message ${if (actionMessage == null) "" else "«$actionMessage»"}".trim()
|
||||||
}
|
}
|
||||||
VkMessage.Action.CHAT_UNPIN_MESSAGE -> {
|
VkMessage.Action.CHAT_UNPIN_MESSAGE -> {
|
||||||
val prefix = when {
|
val prefix = when {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.meloda.fast.api.base
|
package com.meloda.fast.api.base
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import java.io.IOException
|
import com.meloda.fast.api.VKException
|
||||||
|
|
||||||
data class ApiError(
|
data class ApiError(
|
||||||
@SerializedName("error_code")
|
@SerializedName("error_code")
|
||||||
val errorCode: Int,
|
val errorCode: Int,
|
||||||
@SerializedName("error_msg")
|
@SerializedName("error_msg")
|
||||||
override var message: String
|
override var message: String
|
||||||
) : IOException()
|
) : VKException(error = message, code = errorCode)
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ data class VkConversation(
|
|||||||
fun isUser() = type == "user"
|
fun isUser() = type == "user"
|
||||||
fun isGroup() = type == "group"
|
fun isGroup() = type == "group"
|
||||||
|
|
||||||
fun isInUnread() = inRead != lastMessageId
|
fun isInUnread() = inRead < lastMessageId
|
||||||
fun isOutUnread() = outRead != lastMessageId
|
fun isOutUnread() = outRead < lastMessageId
|
||||||
|
|
||||||
fun isUnread() = isInUnread() || isOutUnread()
|
fun isUnread() = isInUnread() || isOutUnread()
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ data class VkGroup(
|
|||||||
val id: Int,
|
val id: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
val screenName: String,
|
val screenName: String,
|
||||||
val photo200: String?
|
val photo200: String?,
|
||||||
|
val membersCount: Int?
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
override fun toString() = name.trim()
|
override fun toString() = name.trim()
|
||||||
|
|||||||
@@ -42,14 +42,31 @@ data class VkMessage(
|
|||||||
|
|
||||||
fun isGroup() = fromId < 0
|
fun isGroup() = fromId < 0
|
||||||
|
|
||||||
fun isRead(conversation: VkConversation) = conversation.outRead < id
|
fun isRead(conversation: VkConversation) =
|
||||||
|
if (isOut) conversation.outRead < id
|
||||||
|
else conversation.inRead < id
|
||||||
|
|
||||||
fun getPreparedAction(): Action? {
|
fun getPreparedAction(): Action? {
|
||||||
if (action == null) return null
|
if (action == null) return null
|
||||||
return Action.parse(action)
|
return Action.parse(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeId(id: Int) = VkMessage(
|
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,
|
id = id,
|
||||||
text = text,
|
text = text,
|
||||||
isOut = isOut,
|
isOut = isOut,
|
||||||
@@ -64,7 +81,10 @@ data class VkMessage(
|
|||||||
actionMessage = actionMessage,
|
actionMessage = actionMessage,
|
||||||
geoType = geoType,
|
geoType = geoType,
|
||||||
important = important
|
important = important
|
||||||
)
|
).also {
|
||||||
|
it.attachments = attachments
|
||||||
|
it.forwards = forwards
|
||||||
|
}
|
||||||
|
|
||||||
enum class Action(val value: String) {
|
enum class Action(val value: String) {
|
||||||
CHAT_CREATE("chat_create"),
|
CHAT_CREATE("chat_create"),
|
||||||
|
|||||||
@@ -13,9 +13,13 @@ data class VkUser(
|
|||||||
val firstName: String,
|
val firstName: String,
|
||||||
val lastName: String,
|
val lastName: String,
|
||||||
val online: Boolean,
|
val online: Boolean,
|
||||||
val photo200: String?
|
val photo200: String?,
|
||||||
|
val lastSeen: Int?,
|
||||||
|
val lastSeenStatus: String?
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
override fun toString() = "$firstName $lastName".trim()
|
override fun toString() = fullName
|
||||||
|
|
||||||
|
val fullName get() = "$firstName $lastName".trim()
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,23 @@
|
|||||||
package com.meloda.fast.api.model.attachments
|
package com.meloda.fast.api.model.attachments
|
||||||
|
|
||||||
|
import com.meloda.fast.api.model.base.attachments.BaseVkSticker
|
||||||
|
import com.meloda.fast.api.model.base.attachments.StickerSize
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class VkSticker(
|
data class VkSticker(
|
||||||
val link: String
|
val id: Int,
|
||||||
) : VkAttachment()
|
val productId: Int,
|
||||||
|
val images: List<BaseVkSticker.Image>,
|
||||||
|
val backgroundImages: List<BaseVkSticker.Image>
|
||||||
|
) : VkAttachment() {
|
||||||
|
|
||||||
|
fun urlForSize(@StickerSize size: Int): String? {
|
||||||
|
for (image in images) {
|
||||||
|
if (image.width == size) return image.url
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,14 +25,17 @@ data class BaseVkGroup(
|
|||||||
@SerializedName("photo_100")
|
@SerializedName("photo_100")
|
||||||
val photo100: String?,
|
val photo100: String?,
|
||||||
@SerializedName("photo_200")
|
@SerializedName("photo_200")
|
||||||
val photo200: String?
|
val photo200: String?,
|
||||||
|
@SerializedName("members_count")
|
||||||
|
val membersCount: Int?
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
fun asVkGroup() = VkGroup(
|
fun asVkGroup() = VkGroup(
|
||||||
id = -id,
|
id = -id,
|
||||||
name = name,
|
name = name,
|
||||||
screenName = screenName,
|
screenName = screenName,
|
||||||
photo200 = photo200
|
photo200 = photo200,
|
||||||
|
membersCount = membersCount
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.meloda.fast.api.model.base
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class BaseVkLongPoll(
|
||||||
|
val server: String,
|
||||||
|
val key: String,
|
||||||
|
val ts: Int,
|
||||||
|
val pts: Int
|
||||||
|
) : Parcelable
|
||||||
@@ -52,7 +52,9 @@ data class BaseVkUser(
|
|||||||
firstName = firstName,
|
firstName = firstName,
|
||||||
lastName = lastName,
|
lastName = lastName,
|
||||||
online = online == 1,
|
online = online == 1,
|
||||||
photo200 = photo200
|
photo200 = photo200,
|
||||||
|
lastSeen = onlineInfo?.lastSeen,
|
||||||
|
lastSeenStatus = onlineInfo?.status
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.meloda.fast.api.model.base.attachments
|
package com.meloda.fast.api.model.base.attachments
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.annotation.IntDef
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import com.meloda.fast.api.model.attachments.VkSticker
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -18,6 +20,13 @@ data class BaseVkSticker(
|
|||||||
val animations: List<Animation>?
|
val animations: List<Animation>?
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
|
fun asVkSticker() = VkSticker(
|
||||||
|
id = stickerId,
|
||||||
|
productId = productId,
|
||||||
|
images = images,
|
||||||
|
backgroundImages = imagesWithBackground
|
||||||
|
)
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Image(
|
data class Image(
|
||||||
val width: Int,
|
val width: Int,
|
||||||
@@ -31,5 +40,7 @@ data class BaseVkSticker(
|
|||||||
val url: String
|
val url: String
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@IntDef(64, 128, 256, 352)
|
||||||
|
annotation class StickerSize
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.meloda.fast.api.model.request
|
package com.meloda.fast.api.model.request
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -59,7 +58,6 @@ data class MessagesSendRequest(
|
|||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class MessagesMarkAsImportantRequest(
|
data class MessagesMarkAsImportantRequest(
|
||||||
@SerializedName("message_ids")
|
|
||||||
val messagesIds: List<Int>,
|
val messagesIds: List<Int>,
|
||||||
val important: Boolean
|
val important: Boolean
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
@@ -71,3 +69,16 @@ data class MessagesMarkAsImportantRequest(
|
|||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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,6 +1,6 @@
|
|||||||
package com.meloda.fast.api.network
|
package com.meloda.fast.api.network
|
||||||
|
|
||||||
object ErrorCodes {
|
object VkErrorCodes {
|
||||||
const val UNKNOWN_ERROR = 1
|
const val UNKNOWN_ERROR = 1
|
||||||
const val APP_DISABLED = 2
|
const val APP_DISABLED = 2
|
||||||
const val UNKNOWN_METHOD = 3
|
const val UNKNOWN_METHOD = 3
|
||||||
@@ -41,7 +41,7 @@ object ErrorCodes {
|
|||||||
const val ACCESS_TO_DOC_DENIED = 1153
|
const val ACCESS_TO_DOC_DENIED = 1153
|
||||||
}
|
}
|
||||||
|
|
||||||
object VKErrors {
|
object VkErrors {
|
||||||
const val UNKNOWN = "unknown_error"
|
const val UNKNOWN = "unknown_error"
|
||||||
|
|
||||||
const val NEED_VALIDATION = "need_validation"
|
const val NEED_VALIDATION = "need_validation"
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
package com.meloda.fast.api.network
|
package com.meloda.fast.api.network
|
||||||
|
|
||||||
import com.meloda.fast.api.VKException
|
import com.meloda.fast.api.base.ApiResponse
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okio.IOException
|
import okio.IOException
|
||||||
import okio.Timeout
|
import okio.Timeout
|
||||||
import org.json.JSONObject
|
|
||||||
import retrofit2.*
|
import retrofit2.*
|
||||||
import java.lang.reflect.ParameterizedType
|
import java.lang.reflect.ParameterizedType
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
@@ -76,11 +75,16 @@ internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy)
|
|||||||
) : Callback<T> {
|
) : Callback<T> {
|
||||||
|
|
||||||
override fun onResponse(call: Call<T>, response: Response<T>) {
|
override fun onResponse(call: Call<T>, response: Response<T>) {
|
||||||
val result: Answer<T> = if (response.isSuccessful)
|
val result: Answer<T> =
|
||||||
Answer.Success(response.body() as T)
|
if (response.isSuccessful) {
|
||||||
else Answer.Error(IOException(response.errorBody()?.string() ?: ""))
|
val baseBody = response.body()
|
||||||
|
if (baseBody !is ApiResponse<*>) Answer.Success(baseBody as T)
|
||||||
if (result is Answer.Error) if (checkErrors(call, result)) return
|
else {
|
||||||
|
val body = baseBody as ApiResponse<*>
|
||||||
|
if (body.error != null) Answer.Error(body.error)
|
||||||
|
else Answer.Success(body as T)
|
||||||
|
}
|
||||||
|
} else Answer.Error(IOException(response.errorBody()?.string() ?: ""))
|
||||||
|
|
||||||
callback.onResponse(proxy, Response.success(result))
|
callback.onResponse(proxy, Response.success(result))
|
||||||
}
|
}
|
||||||
@@ -91,23 +95,6 @@ internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy)
|
|||||||
Response.success(Answer.Error(throwable = error))
|
Response.success(Answer.Error(throwable = error))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkErrors(call: Call<T>, result: Answer.Error): Boolean {
|
|
||||||
val json = JSONObject(result.throwable.message ?: "{}")
|
|
||||||
|
|
||||||
return if (json.has("error")) {
|
|
||||||
val error = json.optString("error", "")
|
|
||||||
val description = json.optString("error_description", "")
|
|
||||||
|
|
||||||
val exception = VKException(
|
|
||||||
error = error,
|
|
||||||
description = description,
|
|
||||||
).also { it.json = json }
|
|
||||||
|
|
||||||
onFailure(call, exception)
|
|
||||||
true
|
|
||||||
} else false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun timeout(): Timeout {
|
override fun timeout(): Timeout {
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ object VKUrls {
|
|||||||
const val GetHistory = "$API/messages.getHistory"
|
const val GetHistory = "$API/messages.getHistory"
|
||||||
const val Send = "$API/messages.send"
|
const val Send = "$API/messages.send"
|
||||||
const val MarkAsImportant = "$API/messages.markAsImportant"
|
const val MarkAsImportant = "$API/messages.markAsImportant"
|
||||||
|
const val GetLongPollServer = "$API/messages.getLongPollServer"
|
||||||
|
const val GetLongPollHistory = "$API/messages.getLongPollHistory"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.meloda.fast.api.network.datasource
|
package com.meloda.fast.api.network.datasource
|
||||||
|
|
||||||
import com.meloda.fast.api.model.request.MessagesGetHistoryRequest
|
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.MessagesMarkAsImportantRequest
|
||||||
import com.meloda.fast.api.model.request.MessagesSendRequest
|
import com.meloda.fast.api.model.request.MessagesSendRequest
|
||||||
import com.meloda.fast.api.network.repo.MessagesRepo
|
import com.meloda.fast.api.network.repo.MessagesRepo
|
||||||
@@ -21,4 +22,7 @@ class MessagesDataSource @Inject constructor(
|
|||||||
suspend fun markAsImportant(params: MessagesMarkAsImportantRequest) =
|
suspend fun markAsImportant(params: MessagesMarkAsImportantRequest) =
|
||||||
repo.markAsImportant(params.map)
|
repo.markAsImportant(params.map)
|
||||||
|
|
||||||
|
suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) =
|
||||||
|
repo.getLongPollServer(params.map)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.meloda.fast.api.network.repo
|
||||||
|
|
||||||
|
import com.meloda.fast.api.base.ApiResponse
|
||||||
|
import com.meloda.fast.api.network.Answer
|
||||||
|
import org.json.JSONObject
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Path
|
||||||
|
import retrofit2.http.QueryMap
|
||||||
|
|
||||||
|
interface LongPollRepo {
|
||||||
|
|
||||||
|
@GET("https://{serverUrl}")
|
||||||
|
suspend fun getResponse(
|
||||||
|
@Path("serverUrl") serverUrl: String,
|
||||||
|
@QueryMap params: Map<String, String>
|
||||||
|
): Answer<ApiResponse<JSONObject>>
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.meloda.fast.api.network.repo
|
package com.meloda.fast.api.network.repo
|
||||||
|
|
||||||
import com.meloda.fast.api.base.ApiResponse
|
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.model.response.MessagesGetHistoryResponse
|
||||||
import com.meloda.fast.api.network.Answer
|
import com.meloda.fast.api.network.Answer
|
||||||
import com.meloda.fast.api.network.VKUrls
|
import com.meloda.fast.api.network.VKUrls
|
||||||
@@ -22,4 +23,8 @@ interface MessagesRepo {
|
|||||||
@POST(VKUrls.Messages.MarkAsImportant)
|
@POST(VKUrls.Messages.MarkAsImportant)
|
||||||
suspend fun markAsImportant(@FieldMap params: Map<String, String>): Answer<ApiResponse<List<Int>>>
|
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,10 +1,16 @@
|
|||||||
package com.meloda.fast.base
|
package com.meloda.fast.base
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.annotation.LayoutRes
|
import androidx.annotation.LayoutRes
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.meloda.fast.R
|
||||||
|
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.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.collect
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
@@ -24,6 +30,16 @@ 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
|
||||||
|
).show()
|
||||||
|
|
||||||
|
UserConfig.clear()
|
||||||
|
requireActivity().finishAffinity()
|
||||||
|
requireActivity().startActivity(Intent(requireContext(), MainActivity::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,9 +4,10 @@ import android.util.Log
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.meloda.fast.api.VKException
|
import com.meloda.fast.api.VKException
|
||||||
|
import com.meloda.fast.api.base.ApiError
|
||||||
import com.meloda.fast.api.network.Answer
|
import com.meloda.fast.api.network.Answer
|
||||||
import com.meloda.fast.api.network.VKErrors
|
import com.meloda.fast.api.network.VkErrorCodes
|
||||||
import com.meloda.fast.util.Utils
|
import com.meloda.fast.api.network.VkErrors
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -26,23 +27,32 @@ abstract class BaseViewModel : ViewModel() {
|
|||||||
onStart?.invoke()
|
onStart?.invoke()
|
||||||
when (val response = job()) {
|
when (val response = job()) {
|
||||||
is Answer.Success -> onAnswer(response.data)
|
is Answer.Success -> onAnswer(response.data)
|
||||||
is Answer.Error -> onError?.invoke(response.throwable)
|
is Answer.Error -> {
|
||||||
|
checkErrors(response.throwable)
|
||||||
|
onError?.invoke(response.throwable)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.also { it.invokeOnCompletion { viewModelScope.launch { onEnd?.invoke() } } }
|
}.also { it.invokeOnCompletion { viewModelScope.launch { onEnd?.invoke() } } }
|
||||||
|
|
||||||
protected suspend fun <T : VKEvent> sendEvent(event: T) = tasksEventChannel.send(event)
|
protected suspend fun <T : VKEvent> sendEvent(event: T) = tasksEventChannel.send(event)
|
||||||
|
|
||||||
protected suspend fun checkErrors(throwable: Throwable) {
|
private suspend fun checkErrors(throwable: Throwable) {
|
||||||
// TODO: 8/31/2021 check illegal token
|
if (throwable is ApiError) {
|
||||||
if (throwable is VKException) {
|
when (throwable.errorCode) {
|
||||||
|
VkErrorCodes.USER_AUTHORIZATION_FAILED -> {
|
||||||
|
sendEvent(IllegalTokenEvent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (throwable is VKException) {
|
||||||
when (throwable.error) {
|
when (throwable.error) {
|
||||||
VKErrors.NEED_CAPTCHA -> {
|
VkErrors.NEED_CAPTCHA -> {
|
||||||
throwable.captcha =
|
throwable.captcha =
|
||||||
(throwable.json?.optString("captcha_sid")
|
(throwable.json?.optString("captcha_sid")
|
||||||
?: "") to (throwable.json?.optString("captcha_img") ?: "")
|
?: "") to (throwable.json?.optString("captcha_img") ?: "")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
VKErrors.NEED_VALIDATION -> {
|
VkErrors.NEED_VALIDATION -> {
|
||||||
throwable.validationSid = throwable.json?.optString("validation_sid")
|
throwable.validationSid = throwable.json?.optString("validation_sid")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -51,7 +61,7 @@ abstract class BaseViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasksEventChannel.send(ShowDialogInfoEvent(null, Log.getStackTraceString(throwable)))
|
sendEvent(ShowDialogInfoEvent(null, Log.getStackTraceString(throwable)))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,10 @@ data class ShowDialogInfoEvent(
|
|||||||
val negativeBtn: String? = null
|
val negativeBtn: String? = null
|
||||||
) : VKEvent()
|
) : VKEvent()
|
||||||
|
|
||||||
|
data class ErrorEvent(val errorText: String) : VKEvent()
|
||||||
|
|
||||||
|
object IllegalTokenEvent : VKEvent()
|
||||||
|
|
||||||
object StartProgressEvent : VKEvent()
|
object StartProgressEvent : VKEvent()
|
||||||
object StopProgressEvent : VKEvent()
|
object StopProgressEvent : VKEvent()
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import com.meloda.fast.database.dao.UsersDao
|
|||||||
VkUser::class,
|
VkUser::class,
|
||||||
VkGroup::class
|
VkGroup::class
|
||||||
],
|
],
|
||||||
version = 16,
|
version = 18,
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|||||||
@@ -2,16 +2,13 @@ package com.meloda.fast.di
|
|||||||
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.meloda.fast.api.network.AuthInterceptor
|
||||||
|
import com.meloda.fast.api.network.ResultCallFactory
|
||||||
import com.meloda.fast.api.network.datasource.AuthDataSource
|
import com.meloda.fast.api.network.datasource.AuthDataSource
|
||||||
import com.meloda.fast.api.network.datasource.ConversationsDataSource
|
import com.meloda.fast.api.network.datasource.ConversationsDataSource
|
||||||
import com.meloda.fast.api.network.datasource.MessagesDataSource
|
import com.meloda.fast.api.network.datasource.MessagesDataSource
|
||||||
import com.meloda.fast.api.network.datasource.UsersDataSource
|
import com.meloda.fast.api.network.datasource.UsersDataSource
|
||||||
import com.meloda.fast.api.network.AuthInterceptor
|
import com.meloda.fast.api.network.repo.*
|
||||||
import com.meloda.fast.api.network.ResultCallFactory
|
|
||||||
import com.meloda.fast.api.network.repo.AuthRepo
|
|
||||||
import com.meloda.fast.api.network.repo.ConversationsRepo
|
|
||||||
import com.meloda.fast.api.network.repo.MessagesRepo
|
|
||||||
import com.meloda.fast.api.network.repo.UsersRepo
|
|
||||||
import com.meloda.fast.database.dao.ConversationsDao
|
import com.meloda.fast.database.dao.ConversationsDao
|
||||||
import com.meloda.fast.database.dao.MessagesDao
|
import com.meloda.fast.database.dao.MessagesDao
|
||||||
import com.meloda.fast.database.dao.UsersDao
|
import com.meloda.fast.database.dao.UsersDao
|
||||||
@@ -81,6 +78,10 @@ object NetworkModule {
|
|||||||
fun provideMessagesRepo(retrofit: Retrofit): MessagesRepo =
|
fun provideMessagesRepo(retrofit: Retrofit): MessagesRepo =
|
||||||
retrofit.create(MessagesRepo::class.java)
|
retrofit.create(MessagesRepo::class.java)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
fun provideLongPollRepo(retrofit: Retrofit): LongPollRepo =
|
||||||
|
retrofit.create(LongPollRepo::class.java)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideAuthDataSource(
|
fun provideAuthDataSource(
|
||||||
|
|||||||
@@ -88,9 +88,10 @@ class ConversationsAdapter constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.avatar.isVisible = avatar != null
|
binding.avatar.isVisible = avatar != null
|
||||||
binding.avatarPlaceholder.isVisible = avatar == null
|
|
||||||
|
|
||||||
if (avatar == null) {
|
if (avatar == null) {
|
||||||
|
binding.avatarPlaceholder.isVisible = true
|
||||||
|
|
||||||
if (conversation.ownerId == VKConstants.FAST_GROUP_ID) {
|
if (conversation.ownerId == VKConstants.FAST_GROUP_ID) {
|
||||||
binding.placeholderBack.setImageDrawable(
|
binding.placeholderBack.setImageDrawable(
|
||||||
ColorDrawable(
|
ColorDrawable(
|
||||||
@@ -114,7 +115,13 @@ class ConversationsAdapter constructor(
|
|||||||
binding.avatar.setImageDrawable(null)
|
binding.avatar.setImageDrawable(null)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
binding.avatar.load(avatar) { crossfade(200) }
|
binding.avatar.load(avatar) {
|
||||||
|
crossfade(200)
|
||||||
|
target {
|
||||||
|
binding.avatarPlaceholder.isVisible = false
|
||||||
|
binding.avatar.setImageDrawable(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.online.isVisible = chatUser?.online == true
|
binding.online.isVisible = chatUser?.online == true
|
||||||
@@ -155,7 +162,8 @@ class ConversationsAdapter constructor(
|
|||||||
message = message
|
message = message
|
||||||
) else null
|
) else null
|
||||||
|
|
||||||
val messageText = (if (actionMessage != null ||
|
val messageText = (if (
|
||||||
|
actionMessage != null ||
|
||||||
forwardsMessage != null ||
|
forwardsMessage != null ||
|
||||||
attachmentText != null
|
attachmentText != null
|
||||||
) ""
|
) ""
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class ConversationsFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var isPaused = false
|
private var isPaused = false
|
||||||
|
private var isExpanded = true
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
@@ -63,7 +64,9 @@ class ConversationsFragment :
|
|||||||
it?.let { user -> binding.avatar.load(user.photo200) { crossfade(100) } }
|
it?.let { user -> binding.avatar.load(user.photo200) { crossfade(100) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset ->
|
binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
|
||||||
|
if (isPaused) return@OnOffsetChangedListener
|
||||||
|
|
||||||
if (verticalOffset <= -100) {
|
if (verticalOffset <= -100) {
|
||||||
binding.avatarContainer.alpha = 0f
|
binding.avatarContainer.alpha = 0f
|
||||||
return@OnOffsetChangedListener
|
return@OnOffsetChangedListener
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.view.KeyEvent
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.viewbinding.library.fragment.viewBinding
|
import android.viewbinding.library.fragment.viewBinding
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
@@ -48,6 +49,11 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
|||||||
private var captchaInputLayout: TextInputLayout? = null
|
private var captchaInputLayout: TextInputLayout? = null
|
||||||
private var validationInputLayout: TextInputLayout? = null
|
private var validationInputLayout: TextInputLayout? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
viewModel.unknownErrorDefaultText = getString(R.string.unknown_error_occurred)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
@@ -156,7 +162,6 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 7/27/2021 extract strings to resources
|
|
||||||
private fun validateInputData(
|
private fun validateInputData(
|
||||||
loginString: String?,
|
loginString: String?,
|
||||||
passwordString: String?,
|
passwordString: String?,
|
||||||
@@ -167,22 +172,22 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
|||||||
|
|
||||||
if (loginString?.isEmpty() == true) {
|
if (loginString?.isEmpty() == true) {
|
||||||
isValidated = false
|
isValidated = false
|
||||||
setError("Input login", binding.loginLayout)
|
setError(getString(R.string.input_login_hint), binding.loginLayout)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (passwordString?.isEmpty() == true) {
|
if (passwordString?.isEmpty() == true) {
|
||||||
isValidated = false
|
isValidated = false
|
||||||
setError("Input password", binding.passwordLayout)
|
setError(getString(R.string.input_password_hint), binding.passwordLayout)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (captchaCode?.isEmpty() == true && captchaInputLayout != null) {
|
if (captchaCode?.isEmpty() == true && captchaInputLayout != null) {
|
||||||
isValidated = false
|
isValidated = false
|
||||||
setError("Input code", captchaInputLayout!!)
|
setError(getString(R.string.input_code_hint), captchaInputLayout!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validationCode?.isEmpty() == true && validationInputLayout != null) {
|
if (validationCode?.isEmpty() == true && validationInputLayout != null) {
|
||||||
isValidated = false
|
isValidated = false
|
||||||
setError("Input code", validationInputLayout!!)
|
setError(getString(R.string.input_code_hint), validationInputLayout!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
return isValidated
|
return isValidated
|
||||||
@@ -281,9 +286,8 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
|||||||
validationBinding.cancel.setOnClickListener { dialog.dismiss() }
|
validationBinding.cancel.setOnClickListener { dialog.dismiss() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 8/31/2021 show snackbar
|
|
||||||
private fun showValidationRequired() {
|
private fun showValidationRequired() {
|
||||||
|
Toast.makeText(requireContext(), R.string.validation_required, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showErrorSnackbar(errorDescription: String) {
|
private fun showErrorSnackbar(errorDescription: String) {
|
||||||
|
|||||||
@@ -6,12 +6,9 @@ import com.meloda.fast.api.UserConfig
|
|||||||
import com.meloda.fast.api.VKConstants
|
import com.meloda.fast.api.VKConstants
|
||||||
import com.meloda.fast.api.VKException
|
import com.meloda.fast.api.VKException
|
||||||
import com.meloda.fast.api.VkUtils
|
import com.meloda.fast.api.VkUtils
|
||||||
import com.meloda.fast.api.network.datasource.AuthDataSource
|
|
||||||
import com.meloda.fast.api.model.request.RequestAuthDirect
|
import com.meloda.fast.api.model.request.RequestAuthDirect
|
||||||
import com.meloda.fast.base.viewmodel.BaseViewModel
|
import com.meloda.fast.api.network.datasource.AuthDataSource
|
||||||
import com.meloda.fast.base.viewmodel.StartProgressEvent
|
import com.meloda.fast.base.viewmodel.*
|
||||||
import com.meloda.fast.base.viewmodel.StopProgressEvent
|
|
||||||
import com.meloda.fast.base.viewmodel.VKEvent
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -21,6 +18,8 @@ class LoginViewModel @Inject constructor(
|
|||||||
private val dataSource: AuthDataSource
|
private val dataSource: AuthDataSource
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
lateinit var unknownErrorDefaultText: String
|
||||||
|
|
||||||
fun login(
|
fun login(
|
||||||
login: String,
|
login: String,
|
||||||
password: String,
|
password: String,
|
||||||
@@ -45,8 +44,8 @@ class LoginViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
onAnswer = {
|
onAnswer = {
|
||||||
// TODO: 8/31/2021 do something
|
|
||||||
if (it.userId == null || it.accessToken == null) {
|
if (it.userId == null || it.accessToken == null) {
|
||||||
|
sendEvent(ErrorEvent(unknownErrorDefaultText))
|
||||||
return@makeJob
|
return@makeJob
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +55,6 @@ class LoginViewModel @Inject constructor(
|
|||||||
sendEvent(SuccessAuth(haveAuthorized = true))
|
sendEvent(SuccessAuth(haveAuthorized = true))
|
||||||
},
|
},
|
||||||
onError = {
|
onError = {
|
||||||
checkErrors(it)
|
|
||||||
if (it !is VKException) return@makeJob
|
if (it !is VKException) return@makeJob
|
||||||
|
|
||||||
twoFaCode?.let { sendEvent(CodeSent) }
|
twoFaCode?.let { sendEvent(CodeSent) }
|
||||||
|
|||||||
@@ -21,9 +21,8 @@ class MainFragment : BaseViewModelFragment<MainViewModel>(R.layout.fragment_main
|
|||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
if (savedInstanceState == null) setupBottomBar()
|
|
||||||
|
|
||||||
if (!UserConfig.isLoggedIn()) findNavController().navigate(R.id.toLogin)
|
if (!UserConfig.isLoggedIn()) findNavController().navigate(R.id.toLogin)
|
||||||
|
else if (savedInstanceState == null) setupBottomBar()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupBottomBar() {
|
private fun setupBottomBar() {
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
package com.meloda.fast.screens.messages
|
package com.meloda.fast.screens.messages
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.view.Gravity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.Toast
|
||||||
import androidx.appcompat.widget.LinearLayoutCompat
|
import androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import coil.load
|
import coil.load
|
||||||
@@ -15,9 +19,9 @@ import com.meloda.fast.api.model.VkGroup
|
|||||||
import com.meloda.fast.api.model.VkMessage
|
import com.meloda.fast.api.model.VkMessage
|
||||||
import com.meloda.fast.api.model.VkUser
|
import com.meloda.fast.api.model.VkUser
|
||||||
import com.meloda.fast.api.model.attachments.VkPhoto
|
import com.meloda.fast.api.model.attachments.VkPhoto
|
||||||
|
import com.meloda.fast.api.model.attachments.VkSticker
|
||||||
import com.meloda.fast.base.adapter.BaseAdapter
|
import com.meloda.fast.base.adapter.BaseAdapter
|
||||||
import com.meloda.fast.base.adapter.BaseHolder
|
import com.meloda.fast.base.adapter.BaseHolder
|
||||||
import com.meloda.fast.common.AppGlobal
|
|
||||||
import com.meloda.fast.databinding.*
|
import com.meloda.fast.databinding.*
|
||||||
import com.meloda.fast.util.AndroidUtils
|
import com.meloda.fast.util.AndroidUtils
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -31,30 +35,31 @@ class MessagesHistoryAdapter constructor(
|
|||||||
) : BaseAdapter<VkMessage, MessagesHistoryAdapter.Holder>(context, values, COMPARATOR) {
|
) : BaseAdapter<VkMessage, MessagesHistoryAdapter.Holder>(context, values, COMPARATOR) {
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
var viewType: Int = when {
|
when {
|
||||||
isPositionHeader(position) -> HEADER
|
isPositionHeader(position) -> return HEADER
|
||||||
isPositionFooter(position) -> FOOTER
|
isPositionFooter(position) -> return FOOTER
|
||||||
else -> -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewType == -1) {
|
getItem(position).let { message ->
|
||||||
getItem(position).let {
|
if (message.action != null) return SERVICE
|
||||||
if (it.action != null) viewType = SERVICE
|
|
||||||
|
|
||||||
val attachments = it.attachments ?: return@let
|
if (!message.attachments.isNullOrEmpty()) {
|
||||||
if (attachments.isEmpty()) return@let
|
val attachments = message.attachments ?: return@let
|
||||||
if (VkUtils.isAttachmentsHaveOneType(attachments) &&
|
if (VkUtils.isAttachmentsHaveOneType(attachments) &&
|
||||||
attachments[0] is VkPhoto
|
attachments[0] is VkPhoto
|
||||||
) {
|
) return if (message.isOut) ATTACHMENT_PHOTOS_OUT
|
||||||
return if (it.isOut) ATTACHMENT_PHOTOS_OUT else ATTACHMENT_PHOTOS_IN
|
else ATTACHMENT_PHOTOS_IN
|
||||||
|
|
||||||
|
|
||||||
|
if (attachments[0] is VkSticker) return if (message.isOut) ATTACHMENT_STICKER_OUT
|
||||||
|
else ATTACHMENT_STICKER_IN
|
||||||
}
|
}
|
||||||
|
|
||||||
if (it.isOut) viewType = OUTGOING
|
if (message.isOut) return OUTGOING
|
||||||
if (!it.isOut) viewType = INCOMING
|
if (!message.isOut) return INCOMING
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return viewType
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isPositionHeader(position: Int) = position == 0
|
private fun isPositionHeader(position: Int) = position == 0
|
||||||
@@ -67,11 +72,17 @@ class MessagesHistoryAdapter constructor(
|
|||||||
SERVICE -> ServiceMessage(
|
SERVICE -> ServiceMessage(
|
||||||
ItemMessageServiceBinding.inflate(inflater, parent, false)
|
ItemMessageServiceBinding.inflate(inflater, parent, false)
|
||||||
)
|
)
|
||||||
|
ATTACHMENT_STICKER_IN -> AttachmentStickerIncoming(
|
||||||
|
ItemMessageAttachmentStickerInBinding.inflate(inflater, parent, false)
|
||||||
|
)
|
||||||
|
ATTACHMENT_STICKER_OUT -> AttachmentStickerOutgoing(
|
||||||
|
ItemMessageAttachmentStickerOutBinding.inflate(inflater, parent, false)
|
||||||
|
)
|
||||||
ATTACHMENT_PHOTOS_IN -> AttachmentPhotosIncoming(
|
ATTACHMENT_PHOTOS_IN -> AttachmentPhotosIncoming(
|
||||||
ItemMessageAttachmentPhotoInBinding.inflate(inflater, parent, false)
|
ItemMessageAttachmentPhotosInBinding.inflate(inflater, parent, false)
|
||||||
)
|
)
|
||||||
ATTACHMENT_PHOTOS_OUT -> AttachmentPhotosOutgoing(
|
ATTACHMENT_PHOTOS_OUT -> AttachmentPhotosOutgoing(
|
||||||
ItemMessageAttachmentPhotoOutBinding.inflate(inflater, parent, false)
|
ItemMessageAttachmentPhotosOutBinding.inflate(inflater, parent, false)
|
||||||
)
|
)
|
||||||
OUTGOING -> OutgoingMessage(
|
OUTGOING -> OutgoingMessage(
|
||||||
ItemMessageOutBinding.inflate(inflater, parent, false)
|
ItemMessageOutBinding.inflate(inflater, parent, false)
|
||||||
@@ -79,7 +90,7 @@ class MessagesHistoryAdapter constructor(
|
|||||||
INCOMING -> IncomingMessage(
|
INCOMING -> IncomingMessage(
|
||||||
ItemMessageInBinding.inflate(inflater, parent, false)
|
ItemMessageInBinding.inflate(inflater, parent, false)
|
||||||
)
|
)
|
||||||
else -> Holder()
|
else -> throw IllegalStateException("Wrong viewType: $viewType")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,56 +118,126 @@ class MessagesHistoryAdapter constructor(
|
|||||||
|
|
||||||
inner class Footer(v: View) : Holder(v)
|
inner class Footer(v: View) : Holder(v)
|
||||||
|
|
||||||
inner class AttachmentPhotosIncoming(
|
inner class IncomingMessage(
|
||||||
private val binding: ItemMessageAttachmentPhotoInBinding
|
private val binding: ItemMessageInBinding
|
||||||
) : Holder(binding.root) {
|
) : Holder(binding.root) {
|
||||||
|
|
||||||
|
private val backgroundNormal =
|
||||||
|
ContextCompat.getDrawable(context, R.drawable.ic_message_in_background)
|
||||||
|
private val backgroundMiddle =
|
||||||
|
ContextCompat.getDrawable(context, R.drawable.ic_message_in_background_middle)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
binding.photo.shapeAppearanceModel = binding.photo.shapeAppearanceModel.withCornerSize {
|
MessagesManager.setRootMaxWidth(binding.bubble)
|
||||||
AndroidUtils.px(12)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(position: Int) {
|
override fun bind(position: Int) {
|
||||||
val message = getItem(position)
|
val message = getItem(position)
|
||||||
|
|
||||||
val photo = message.attachments?.get(0) as? VkPhoto ?: return
|
val prevMessage = getOrNull(position - 1)
|
||||||
|
val nextMessage = getOrNull(position + 1)
|
||||||
|
|
||||||
val size = photo.sizeOfType('m') ?: return
|
binding.unread.isVisible = message.isRead(conversation)
|
||||||
|
|
||||||
binding.photo.layoutParams = FrameLayout.LayoutParams(
|
binding.bubble.background =
|
||||||
AndroidUtils.px(size.width).roundToInt(),
|
if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundNormal
|
||||||
AndroidUtils.px(size.height).roundToInt()
|
else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddle
|
||||||
|
else backgroundNormal
|
||||||
|
|
||||||
|
if (!message.isPeerChat()) {
|
||||||
|
binding.title.isVisible = false
|
||||||
|
binding.avatar.isVisible = false
|
||||||
|
|
||||||
|
binding.spacer.isVisible =
|
||||||
|
!(prevMessage != null && prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60)
|
||||||
|
} else {
|
||||||
|
binding.title.isVisible =
|
||||||
|
if (prevMessage == null || prevMessage.fromId != message.fromId) message.isPeerChat()
|
||||||
|
else message.date - prevMessage.date >= 60
|
||||||
|
|
||||||
|
binding.spacer.isVisible = binding.title.isVisible
|
||||||
|
|
||||||
|
binding.avatar.visibility =
|
||||||
|
if (nextMessage == null || nextMessage.fromId != message.fromId) if (message.isPeerChat()) View.VISIBLE else View.GONE
|
||||||
|
else if (nextMessage.date - message.date >= 60) View.VISIBLE
|
||||||
|
else View.INVISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
val messageUser: VkUser? = if (message.isUser()) {
|
||||||
|
profiles[message.fromId]
|
||||||
|
} else null
|
||||||
|
|
||||||
|
val messageGroup: VkGroup? = if (message.isGroup()) {
|
||||||
|
groups[message.fromId]
|
||||||
|
} else null
|
||||||
|
|
||||||
|
MessagesManager.loadMessageAvatar(
|
||||||
|
message = message,
|
||||||
|
messageUser = messageUser,
|
||||||
|
messageGroup = messageGroup,
|
||||||
|
imageView = binding.avatar
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.photo.load(size.url)
|
val title = when {
|
||||||
|
message.isUser() && messageUser != null -> messageUser.firstName
|
||||||
|
message.isGroup() && messageGroup != null -> messageGroup.name
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.title.text = title
|
||||||
|
binding.title.measure(0, 0)
|
||||||
|
|
||||||
|
if (binding.title.isVisible) {
|
||||||
|
binding.bubble.minimumWidth = binding.title.measuredWidth + 60
|
||||||
|
} else {
|
||||||
|
binding.bubble.minimumWidth = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class AttachmentPhotosOutgoing(
|
MessagesManager.setMessageText(
|
||||||
private val binding: ItemMessageAttachmentPhotoOutBinding
|
message = message,
|
||||||
|
textView = binding.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class OutgoingMessage(
|
||||||
|
private val binding: ItemMessageOutBinding
|
||||||
) : Holder(binding.root) {
|
) : Holder(binding.root) {
|
||||||
|
|
||||||
|
private val backgroundNormal =
|
||||||
|
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background)
|
||||||
|
private val backgroundMiddle =
|
||||||
|
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle)
|
||||||
|
private val backgroundStroke =
|
||||||
|
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_stroke)
|
||||||
|
private val backgroundMiddleStroke =
|
||||||
|
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle_stroke)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
binding.photo.shapeAppearanceModel = binding.photo.shapeAppearanceModel.withCornerSize {
|
MessagesManager.setRootMaxWidth(binding.bubble)
|
||||||
AndroidUtils.px(12)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(position: Int) {
|
override fun bind(position: Int) {
|
||||||
val message = getItem(position)
|
val message = getItem(position)
|
||||||
|
|
||||||
val photo = message.attachments?.get(0) as? VkPhoto ?: return
|
val prevMessage = getOrNull(position - 1)
|
||||||
|
|
||||||
val size = photo.sizeOfType('m') ?: return
|
binding.text.text = message.text ?: "[no_message]"
|
||||||
|
|
||||||
binding.photo.layoutParams = LinearLayoutCompat.LayoutParams(
|
binding.unread.isVisible = message.isRead(conversation)
|
||||||
AndroidUtils.px(size.width).roundToInt(),
|
|
||||||
AndroidUtils.px(size.height).roundToInt()
|
|
||||||
)
|
|
||||||
|
|
||||||
binding.photo.load(size.url)
|
binding.spacer.isVisible =
|
||||||
|
!(prevMessage != null && prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60)
|
||||||
|
|
||||||
|
binding.bubble.background =
|
||||||
|
if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundNormal
|
||||||
|
else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddle
|
||||||
|
else backgroundNormal
|
||||||
|
|
||||||
|
binding.bubbleStroke.background =
|
||||||
|
if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundStroke
|
||||||
|
else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleStroke
|
||||||
|
else backgroundStroke
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -167,6 +248,12 @@ class MessagesHistoryAdapter constructor(
|
|||||||
|
|
||||||
private val youPrefix = context.getString(R.string.you_message_prefix)
|
private val youPrefix = context.getString(R.string.you_message_prefix)
|
||||||
|
|
||||||
|
init {
|
||||||
|
binding.photo.shapeAppearanceModel.run {
|
||||||
|
withCornerSize { AndroidUtils.px(4) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun bind(position: Int) {
|
override fun bind(position: Int) {
|
||||||
val message = getItem(position)
|
val message = getItem(position)
|
||||||
|
|
||||||
@@ -188,42 +275,104 @@ class MessagesHistoryAdapter constructor(
|
|||||||
messageUser = messageUser,
|
messageUser = messageUser,
|
||||||
messageGroup = messageGroup
|
messageGroup = messageGroup
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val attachments = message.attachments ?: return
|
||||||
|
attachments[0].let { attachment ->
|
||||||
|
if (attachment !is VkPhoto) return@let
|
||||||
|
|
||||||
|
binding.photo.isVisible = true
|
||||||
|
|
||||||
|
val size = attachment.sizeOfType('m') ?: return@let
|
||||||
|
|
||||||
|
binding.photo.layoutParams = LinearLayoutCompat.LayoutParams(
|
||||||
|
size.width,
|
||||||
|
size.height
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.photo.load(size.url) {
|
||||||
|
crossfade(150)
|
||||||
|
fallback(ColorDrawable(Color.LTGRAY))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class OutgoingMessage(
|
inner class AttachmentPhotosIncoming(
|
||||||
private val binding: ItemMessageOutBinding
|
private val binding: ItemMessageAttachmentPhotosInBinding
|
||||||
) : Holder(binding.root) {
|
) : Holder(binding.root) {
|
||||||
|
|
||||||
init {
|
|
||||||
binding.bubble.maxWidth = (AppGlobal.screenWidth * 0.75).roundToInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bind(position: Int) {
|
override fun bind(position: Int) {
|
||||||
val message = getItem(position)
|
val message = getItem(position)
|
||||||
|
|
||||||
binding.text.text = message.text ?: "[no_message]"
|
MessagesManager.loadPhotos(
|
||||||
|
context = context,
|
||||||
binding.unread.isVisible = message.isRead(conversation)
|
message = message,
|
||||||
|
binding.photosContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
inner class AttachmentPhotosOutgoing(
|
||||||
|
private val binding: ItemMessageAttachmentPhotosOutBinding
|
||||||
inner class IncomingMessage(
|
|
||||||
private val binding: ItemMessageInBinding
|
|
||||||
) : Holder(binding.root) {
|
) : Holder(binding.root) {
|
||||||
|
|
||||||
init {
|
|
||||||
binding.bubble.maxWidth = (AppGlobal.screenWidth * 0.7).roundToInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bind(position: Int) {
|
override fun bind(position: Int) {
|
||||||
val message = getItem(position)
|
val message = getItem(position)
|
||||||
|
|
||||||
|
MessagesManager.loadPhotos(
|
||||||
|
context = context,
|
||||||
|
message = message,
|
||||||
|
photosContainer = binding.photosContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class AttachmentStickerOutgoing(
|
||||||
|
private val binding: ItemMessageAttachmentStickerOutBinding
|
||||||
|
) : Holder(binding.root) {
|
||||||
|
|
||||||
|
override fun bind(position: Int) {
|
||||||
|
val message = getItem(position)
|
||||||
val prevMessage = getOrNull(position - 1)
|
val prevMessage = getOrNull(position - 1)
|
||||||
val nextMessage = getOrNull(position + 1)
|
val nextMessage = getOrNull(position + 1)
|
||||||
|
|
||||||
binding.title.isVisible =
|
if (!message.isPeerChat()) {
|
||||||
|
binding.spacer.isVisible =
|
||||||
|
!(prevMessage != null && prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60)
|
||||||
|
} else {
|
||||||
|
binding.spacer.isVisible =
|
||||||
|
if (prevMessage == null || prevMessage.fromId != message.fromId) message.isPeerChat()
|
||||||
|
else message.date - prevMessage.date >= 60
|
||||||
|
}
|
||||||
|
|
||||||
|
val sticker = message.attachments?.get(0) as? VkSticker ?: return
|
||||||
|
val url = sticker.urlForSize(352)!!
|
||||||
|
|
||||||
|
binding.photo.layoutParams.also {
|
||||||
|
it.width = 352
|
||||||
|
it.height = 352
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.photo.load(url) { crossfade(150) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class AttachmentStickerIncoming(
|
||||||
|
private val binding: ItemMessageAttachmentStickerInBinding
|
||||||
|
) : Holder(binding.root) {
|
||||||
|
|
||||||
|
override fun bind(position: Int) {
|
||||||
|
val message = getItem(position)
|
||||||
|
val prevMessage = getOrNull(position - 1)
|
||||||
|
val nextMessage = getOrNull(position + 1)
|
||||||
|
|
||||||
|
if (!message.isPeerChat()) {
|
||||||
|
binding.avatar.isVisible = false
|
||||||
|
|
||||||
|
binding.spacer.isVisible =
|
||||||
|
!(prevMessage != null && prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60)
|
||||||
|
} else {
|
||||||
|
binding.spacer.isVisible =
|
||||||
if (prevMessage == null || prevMessage.fromId != message.fromId) message.isPeerChat()
|
if (prevMessage == null || prevMessage.fromId != message.fromId) message.isPeerChat()
|
||||||
else message.date - prevMessage.date >= 60
|
else message.date - prevMessage.date >= 60
|
||||||
|
|
||||||
@@ -231,6 +380,7 @@ class MessagesHistoryAdapter constructor(
|
|||||||
if (nextMessage == null || nextMessage.fromId != message.fromId) if (message.isPeerChat()) View.VISIBLE else View.GONE
|
if (nextMessage == null || nextMessage.fromId != message.fromId) if (message.isPeerChat()) View.VISIBLE else View.GONE
|
||||||
else if (nextMessage.date - message.date >= 60) View.VISIBLE
|
else if (nextMessage.date - message.date >= 60) View.VISIBLE
|
||||||
else View.INVISIBLE
|
else View.INVISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
val messageUser: VkUser? = if (message.isUser()) {
|
val messageUser: VkUser? = if (message.isUser()) {
|
||||||
profiles[message.fromId]
|
profiles[message.fromId]
|
||||||
@@ -246,24 +396,34 @@ class MessagesHistoryAdapter constructor(
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.avatar.load(avatar) { crossfade(100) }
|
||||||
|
|
||||||
val title = when {
|
val title = when {
|
||||||
message.isUser() && messageUser != null -> messageUser.firstName
|
message.isUser() && messageUser != null -> messageUser.fullName
|
||||||
message.isGroup() && messageGroup != null -> messageGroup.name
|
message.isGroup() && messageGroup != null -> messageGroup.name
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.avatar.load(avatar) { crossfade(100) }
|
binding.avatar.setOnLongClickListener {
|
||||||
|
Toast.makeText(context, title, Toast.LENGTH_SHORT).apply {
|
||||||
binding.text.text = message.text ?: "[no_message]"
|
setGravity(
|
||||||
|
Gravity.START or Gravity.BOTTOM,
|
||||||
binding.title.text = title
|
0,
|
||||||
binding.title.measure(0, 0)
|
-50
|
||||||
|
)
|
||||||
if (binding.title.isVisible) {
|
}.show()
|
||||||
binding.bubble.minimumWidth = binding.title.measuredWidth + 60
|
true
|
||||||
} else {
|
|
||||||
binding.bubble.minimumWidth = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val sticker = message.attachments?.get(0) as? VkSticker ?: return
|
||||||
|
val url = sticker.urlForSize(352)!!
|
||||||
|
|
||||||
|
binding.photo.layoutParams.also {
|
||||||
|
it.width = 352
|
||||||
|
it.height = 352
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.photo.load(url) { crossfade(150) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,8 +441,11 @@ class MessagesHistoryAdapter constructor(
|
|||||||
private const val INCOMING = 3
|
private const val INCOMING = 3
|
||||||
private const val OUTGOING = 4
|
private const val OUTGOING = 4
|
||||||
|
|
||||||
|
|
||||||
private const val ATTACHMENT_PHOTOS_IN = 101
|
private const val ATTACHMENT_PHOTOS_IN = 101
|
||||||
private const val ATTACHMENT_PHOTOS_OUT = 1011
|
private const val ATTACHMENT_PHOTOS_OUT = 102
|
||||||
|
private const val ATTACHMENT_STICKER_IN = 111
|
||||||
|
private const val ATTACHMENT_STICKER_OUT = 112
|
||||||
|
|
||||||
private val COMPARATOR = object : DiffUtil.ItemCallback<VkMessage>() {
|
private val COMPARATOR = object : DiffUtil.ItemCallback<VkMessage>() {
|
||||||
override fun areItemsTheSame(
|
override fun areItemsTheSame(
|
||||||
|
|||||||
@@ -80,8 +80,15 @@ class MessagesHistoryFragment :
|
|||||||
|
|
||||||
val status = when {
|
val status = when {
|
||||||
conversation.isChat() -> "${conversation.membersCount} members"
|
conversation.isChat() -> "${conversation.membersCount} members"
|
||||||
conversation.isUser() -> if (user?.online == true) "Online" else "Last seen at [...]"
|
conversation.isUser() -> when {
|
||||||
conversation.isGroup() -> "[Group status]"
|
// TODO: 9/15/2021 user normal time
|
||||||
|
user?.online == true -> "Online"
|
||||||
|
user?.lastSeen != null -> "Last seen at ${
|
||||||
|
SimpleDateFormat("HH:mm", Locale.getDefault()).format(user?.lastSeen!! * 1000L)
|
||||||
|
}"
|
||||||
|
else -> if (user?.lastSeenStatus != null) "Last seen ${user?.lastSeenStatus!!}" else "Last seen recently"
|
||||||
|
}
|
||||||
|
conversation.isGroup() -> if (group?.membersCount != null) "${group?.membersCount} members" else "Group"
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +169,6 @@ class MessagesHistoryFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
action.observe(viewLifecycleOwner) {
|
action.observe(viewLifecycleOwner) {
|
||||||
|
|
||||||
binding.action.animate()
|
binding.action.animate()
|
||||||
.scaleX(1.25f)
|
.scaleX(1.25f)
|
||||||
.scaleY(1.25f)
|
.scaleY(1.25f)
|
||||||
@@ -216,18 +222,19 @@ class MessagesHistoryFragment :
|
|||||||
peerId = conversation.id,
|
peerId = conversation.id,
|
||||||
message = messageText,
|
message = messageText,
|
||||||
randomId = 0
|
randomId = 0
|
||||||
) { message = message.changeId(it) }
|
) { message = message.copyMessage(id = it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEvent(event: VKEvent) {
|
override fun onEvent(event: VKEvent) {
|
||||||
|
super.onEvent(event)
|
||||||
|
|
||||||
when (event) {
|
when (event) {
|
||||||
is MessagesMarkAsImportant -> markMessagesAsImportant(event)
|
is MessagesMarkAsImportant -> markMessagesAsImportant(event)
|
||||||
is MessagesLoaded -> refreshMessages(event)
|
is MessagesLoaded -> refreshMessages(event)
|
||||||
is StartProgressEvent -> onProgressStarted()
|
is StartProgressEvent -> onProgressStarted()
|
||||||
is StopProgressEvent -> onProgressStopped()
|
is StopProgressEvent -> onProgressStopped()
|
||||||
}
|
}
|
||||||
super.onEvent(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onProgressStarted() {
|
private fun onProgressStarted() {
|
||||||
@@ -276,7 +283,9 @@ class MessagesHistoryFragment :
|
|||||||
val message = adapter.values[i]
|
val message = adapter.values[i]
|
||||||
if (event.messagesIds.contains(message.id)) {
|
if (event.messagesIds.contains(message.id)) {
|
||||||
if (!changed) changed = true
|
if (!changed) changed = true
|
||||||
adapter.values[i] = message.copy(important = event.important)
|
adapter.values[i] = message.copyMessage(
|
||||||
|
important = event.important
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,6 +312,7 @@ class MessagesHistoryFragment :
|
|||||||
|
|
||||||
private fun onItemClick(position: Int) {
|
private fun onItemClick(position: Int) {
|
||||||
val message = adapter.values[position]
|
val message = adapter.values[position]
|
||||||
|
if (message.action != null) return
|
||||||
|
|
||||||
val important = if (message.important) "Unmark as important" else "Mark as important"
|
val important = if (message.important) "Unmark as important" else "Mark as important"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.meloda.fast.screens.messages
|
package com.meloda.fast.screens.messages
|
||||||
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.meloda.fast.api.VKConstants
|
||||||
import com.meloda.fast.api.model.VkConversation
|
import com.meloda.fast.api.model.VkConversation
|
||||||
import com.meloda.fast.api.model.VkGroup
|
import com.meloda.fast.api.model.VkGroup
|
||||||
import com.meloda.fast.api.model.VkMessage
|
import com.meloda.fast.api.model.VkMessage
|
||||||
@@ -28,10 +29,10 @@ class MessagesHistoryViewModel @Inject constructor(
|
|||||||
makeJob({
|
makeJob({
|
||||||
dataSource.getHistory(
|
dataSource.getHistory(
|
||||||
MessagesGetHistoryRequest(
|
MessagesGetHistoryRequest(
|
||||||
count = 90,
|
count = 30,
|
||||||
peerId = peerId,
|
peerId = peerId,
|
||||||
extended = true,
|
extended = true,
|
||||||
fields = "photo_200,sex"
|
fields = "${VKConstants.USER_FIELDS},${VKConstants.GROUP_FIELDS}"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package com.meloda.fast.screens.messages
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.Space
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
import androidx.core.view.isNotEmpty
|
||||||
|
import coil.load
|
||||||
|
import com.google.android.material.imageview.ShapeableImageView
|
||||||
|
import com.meloda.fast.api.model.VkGroup
|
||||||
|
import com.meloda.fast.api.model.VkMessage
|
||||||
|
import com.meloda.fast.api.model.VkUser
|
||||||
|
import com.meloda.fast.api.model.attachments.VkPhoto
|
||||||
|
import com.meloda.fast.common.AppGlobal
|
||||||
|
import com.meloda.fast.util.AndroidUtils
|
||||||
|
import com.meloda.fast.widget.BoundedFrameLayout
|
||||||
|
import com.meloda.fast.widget.BoundedLinearLayout
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
object MessagesManager {
|
||||||
|
|
||||||
|
fun setRootMaxWidth(
|
||||||
|
layout: View
|
||||||
|
) {
|
||||||
|
val maxWidth = (AppGlobal.screenWidth * 0.7).roundToInt()
|
||||||
|
|
||||||
|
if (layout is BoundedFrameLayout) {
|
||||||
|
layout.maxWidth = maxWidth
|
||||||
|
} else if (layout is BoundedLinearLayout) {
|
||||||
|
layout.maxWidth = maxWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadPhotos(
|
||||||
|
context: Context,
|
||||||
|
message: VkMessage,
|
||||||
|
photosContainer: LinearLayoutCompat
|
||||||
|
) {
|
||||||
|
photosContainer.removeAllViews()
|
||||||
|
|
||||||
|
message.attachments?.let { attachments ->
|
||||||
|
val photos = attachments.map { it as VkPhoto }
|
||||||
|
|
||||||
|
photos.forEach { photo ->
|
||||||
|
val size = photo.sizeOfType('m') ?: return
|
||||||
|
|
||||||
|
val newPhoto = ShapeableImageView(context).also {
|
||||||
|
it.layoutParams = LinearLayoutCompat.LayoutParams(
|
||||||
|
AndroidUtils.px(size.width).roundToInt(),
|
||||||
|
AndroidUtils.px(size.height).roundToInt()
|
||||||
|
)
|
||||||
|
it.shapeAppearanceModel =
|
||||||
|
it.shapeAppearanceModel.withCornerSize { AndroidUtils.px(4) }
|
||||||
|
it.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||||
|
}
|
||||||
|
|
||||||
|
val spacer = Space(context).also {
|
||||||
|
it.layoutParams = LinearLayoutCompat.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
AndroidUtils.px(5).roundToInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (photosContainer.isNotEmpty())
|
||||||
|
photosContainer.addView(spacer)
|
||||||
|
|
||||||
|
photosContainer.addView(newPhoto)
|
||||||
|
|
||||||
|
newPhoto.load(size.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadMessageAvatar(
|
||||||
|
message: VkMessage,
|
||||||
|
messageUser: VkUser?,
|
||||||
|
messageGroup: VkGroup?,
|
||||||
|
imageView: ImageView
|
||||||
|
) {
|
||||||
|
val avatar = when {
|
||||||
|
message.isUser() && messageUser != null && !messageUser.photo200.isNullOrBlank() -> messageUser.photo200
|
||||||
|
message.isGroup() && messageGroup != null && !messageGroup.photo200.isNullOrBlank() -> messageGroup.photo200
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
imageView.load(avatar) { crossfade(100) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMessageText(
|
||||||
|
message: VkMessage,
|
||||||
|
textView: TextView
|
||||||
|
) {
|
||||||
|
textView.text = message.text ?: "[no_message]"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
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 kotlinx.coroutines.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
class LongPollService {
|
||||||
|
}
|
||||||
|
|
||||||
|
class LongPollTask @Inject constructor(
|
||||||
|
private val dataSource: MessagesDataSource,
|
||||||
|
private val longPollRepo: LongPollRepo
|
||||||
|
) : CoroutineScope {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "LongPollTask"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val job = SupervisorJob()
|
||||||
|
|
||||||
|
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||||
|
Log.d(TAG, "error: $throwable")
|
||||||
|
throwable.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val coroutineContext: CoroutineContext
|
||||||
|
get() = Dispatchers.Default + job + exceptionHandler
|
||||||
|
|
||||||
|
fun startPolling(): Job {
|
||||||
|
if (job.isCompleted || job.isCancelled) throw Exception("Job is over")
|
||||||
|
|
||||||
|
return launch {
|
||||||
|
val serverInfo = dataSource.getLongPollServer(
|
||||||
|
MessagesGetLongPollServerRequest(
|
||||||
|
needPts = true,
|
||||||
|
version = 10
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
println("TESTJOPAAAAAA: $serverInfo")
|
||||||
|
// val response = serverInfo.response ?: return@launch
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ import android.util.AttributeSet
|
|||||||
import android.view.ViewTreeObserver
|
import android.view.ViewTreeObserver
|
||||||
import androidx.appcompat.widget.AppCompatImageView
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
|
|
||||||
// TODO: 8/31/2021 extend ShapeableImageView and set corners for half of size
|
|
||||||
class CircleImageView : AppCompatImageView {
|
class CircleImageView : AppCompatImageView {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -27,7 +26,6 @@ class CircleImageView : AppCompatImageView {
|
|||||||
attrs,
|
attrs,
|
||||||
defStyleAttr
|
defStyleAttr
|
||||||
) {
|
) {
|
||||||
|
|
||||||
init()
|
init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<solid android:color="@android:color/white" />
|
||||||
|
|
||||||
|
<corners
|
||||||
|
android:bottomLeftRadius="5dp"
|
||||||
|
android:bottomRightRadius="40dp"
|
||||||
|
android:topLeftRadius="5dp"
|
||||||
|
android:topRightRadius="40dp" />
|
||||||
|
|
||||||
|
</shape>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:shape="rectangle">
|
android:shape="rectangle">
|
||||||
|
|
||||||
<solid android:color="@android:color/white" />
|
<solid android:color="@color/messageOutColor" />
|
||||||
|
|
||||||
<corners
|
<corners
|
||||||
android:bottomLeftRadius="40dp"
|
android:bottomLeftRadius="40dp"
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<solid android:color="@color/messageOutColor" />
|
||||||
|
|
||||||
|
<corners
|
||||||
|
android:bottomLeftRadius="40dp"
|
||||||
|
android:bottomRightRadius="5dp"
|
||||||
|
android:topLeftRadius="40dp"
|
||||||
|
android:topRightRadius="5dp" />
|
||||||
|
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<solid android:color="@color/messageOutStrokeColor" />
|
||||||
|
|
||||||
|
<corners
|
||||||
|
android:bottomLeftRadius="40dp"
|
||||||
|
android:bottomRightRadius="5dp"
|
||||||
|
android:topLeftRadius="40dp"
|
||||||
|
android:topRightRadius="5dp" />
|
||||||
|
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<solid android:color="@color/messageOutStrokeColor" />
|
||||||
|
|
||||||
|
<corners
|
||||||
|
android:bottomLeftRadius="40dp"
|
||||||
|
android:bottomRightRadius="5dp"
|
||||||
|
android:topLeftRadius="40dp"
|
||||||
|
android:topRightRadius="30dp" />
|
||||||
|
|
||||||
|
</shape>
|
||||||
@@ -14,12 +14,13 @@
|
|||||||
app:elevation="0dp">
|
app:elevation="0dp">
|
||||||
|
|
||||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||||
|
android:id="@+id/collapsingToolbarLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:elevation="0dp"
|
android:elevation="0dp"
|
||||||
app:collapsedTitleTextAppearance="@style/CollapsingToolbarCollapsedTitle"
|
app:collapsedTitleTextAppearance="@style/CollapsingToolbarCollapsedTitle"
|
||||||
app:expandedTitleTextAppearance="@style/CollapsingToolbarTitle"
|
app:expandedTitleTextAppearance="@style/CollapsingToolbarTitle"
|
||||||
app:layout_scrollFlags="scroll|enterAlways|snap"
|
app:layout_scrollFlags="scroll|enterAlwaysCollapsed|snap"
|
||||||
app:title="Messages">
|
app:title="Messages">
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
|||||||
@@ -34,8 +34,7 @@
|
|||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/avatarPlaceholder"
|
android:id="@+id/avatarPlaceholder"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:visibility="gone">
|
|
||||||
|
|
||||||
<com.meloda.fast.widget.CircleImageView
|
<com.meloda.fast.widget.CircleImageView
|
||||||
android:id="@+id/placeholderBack"
|
android:id="@+id/placeholderBack"
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.LinearLayoutCompat
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="end"
|
|
||||||
android:padding="12dp">
|
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
|
||||||
android:id="@+id/photo"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:scaleType="centerCrop"
|
|
||||||
tools:src="@tools:sample/backgrounds/scenic" />
|
|
||||||
|
|
||||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
|
||||||
|
|
||||||
</layout>
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="12dp"
|
||||||
|
android:paddingVertical="2.5dp">
|
||||||
|
|
||||||
|
<com.meloda.fast.widget.CircleImageView
|
||||||
|
android:id="@+id/avatar"
|
||||||
|
android:layout_width="35dp"
|
||||||
|
android:layout_height="35dp"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:id="@+id/spacer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="10dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="14dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:fontFamily="@font/google_sans_regular"
|
||||||
|
android:textColor="@color/a3_700"
|
||||||
|
tools:text="@tools:sample/full_names" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="bottom"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<com.meloda.fast.widget.BoundedFrameLayout
|
||||||
|
android:id="@+id/bubble"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/ic_message_in_background"
|
||||||
|
android:backgroundTint="@color/n2_100"
|
||||||
|
tools:ignore="UselessParent">
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:padding="15dp"
|
||||||
|
android:textColor="@color/n1_800"
|
||||||
|
tools:text="This" />
|
||||||
|
|
||||||
|
</com.meloda.fast.widget.BoundedFrameLayout>
|
||||||
|
|
||||||
|
<com.meloda.fast.widget.CircleImageView
|
||||||
|
android:id="@+id/unread"
|
||||||
|
android:layout_width="13dp"
|
||||||
|
android:layout_height="13dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:src="@color/a3_200" />
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
android:id="@+id/photosContainer"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="5dp"
|
||||||
|
android:orientation="vertical" />
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
|
||||||
|
</layout>
|
||||||
+7
-8
@@ -1,19 +1,18 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
|
|
||||||
<FrameLayout
|
<com.meloda.fast.widget.BoundedFrameLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="12dp">
|
android:padding="12dp">
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
android:id="@+id/photo"
|
android:id="@+id/photosContainer"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:scaleType="centerCrop"
|
android:layout_gravity="end"
|
||||||
tools:src="@tools:sample/backgrounds/scenic" />
|
android:orientation="vertical" />
|
||||||
|
|
||||||
</FrameLayout>
|
</com.meloda.fast.widget.BoundedFrameLayout>
|
||||||
|
|
||||||
</layout>
|
</layout>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="12dp"
|
||||||
|
android:paddingVertical="2.5dp">
|
||||||
|
|
||||||
|
<com.meloda.fast.widget.CircleImageView
|
||||||
|
android:id="@+id/avatar"
|
||||||
|
android:layout_width="35dp"
|
||||||
|
android:layout_height="35dp"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:id="@+id/spacer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="10dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/photo"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:src="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
|
||||||
|
</layout>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="end"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="12dp"
|
||||||
|
android:paddingVertical="2.5dp">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:id="@+id/spacer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="10dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/photo"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:src="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
|
||||||
|
</layout>
|
||||||
@@ -22,6 +22,12 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:id="@+id/spacer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="10dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/title"
|
android:id="@+id/title"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@@ -32,6 +38,12 @@
|
|||||||
android:textColor="@color/a3_700"
|
android:textColor="@color/a3_700"
|
||||||
tools:text="@tools:sample/full_names" />
|
tools:text="@tools:sample/full_names" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="bottom"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<com.meloda.fast.widget.BoundedFrameLayout
|
<com.meloda.fast.widget.BoundedFrameLayout
|
||||||
android:id="@+id/bubble"
|
android:id="@+id/bubble"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@@ -51,9 +63,15 @@
|
|||||||
|
|
||||||
</com.meloda.fast.widget.BoundedFrameLayout>
|
</com.meloda.fast.widget.BoundedFrameLayout>
|
||||||
|
|
||||||
|
<com.meloda.fast.widget.CircleImageView
|
||||||
|
android:id="@+id/unread"
|
||||||
|
android:layout_width="13dp"
|
||||||
|
android:layout_height="13dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginBottom="20dp"
|
||||||
|
android:src="@color/a3_200" />
|
||||||
|
|
||||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
|
||||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
</layout>
|
</layout>
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="end|bottom"
|
android:gravity="end|bottom"
|
||||||
|
android:orientation="horizontal"
|
||||||
android:paddingHorizontal="12dp"
|
android:paddingHorizontal="12dp"
|
||||||
android:paddingVertical="2.5dp">
|
android:paddingVertical="2.5dp">
|
||||||
|
|
||||||
@@ -17,12 +18,22 @@
|
|||||||
android:layout_marginBottom="20dp"
|
android:layout_marginBottom="20dp"
|
||||||
android:src="@color/a3_200" />
|
android:src="@color/a3_200" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:id="@+id/spacer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="10dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/bubbleStroke"
|
android:id="@+id/bubbleStroke"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/ic_message_out_background"
|
android:background="@drawable/ic_message_out_background_stroke"
|
||||||
android:backgroundTint="@color/n2_100"
|
|
||||||
android:padding="1.5dp"
|
android:padding="1.5dp"
|
||||||
tools:ignore="UselessParent">
|
tools:ignore="UselessParent">
|
||||||
|
|
||||||
@@ -31,8 +42,7 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:background="@drawable/ic_message_out_background"
|
android:background="@drawable/ic_message_out_background">
|
||||||
android:backgroundTint="@color/n1_10">
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/text"
|
android:id="@+id/text"
|
||||||
@@ -40,7 +50,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_vertical|start"
|
android:layout_gravity="center_vertical|start"
|
||||||
android:padding="15dp"
|
android:padding="15dp"
|
||||||
android:textColor="@color/n1_800"
|
android:textColor="@color/n1_900"
|
||||||
tools:text="This is test" />
|
tools:text="This is test" />
|
||||||
|
|
||||||
</com.meloda.fast.widget.BoundedFrameLayout>
|
</com.meloda.fast.widget.BoundedFrameLayout>
|
||||||
@@ -49,4 +59,6 @@
|
|||||||
|
|
||||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
|
||||||
</layout>
|
</layout>
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
<androidx.appcompat.widget.LinearLayoutCompat
|
<androidx.appcompat.widget.LinearLayoutCompat
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingHorizontal="12dp"
|
android:paddingHorizontal="12dp"
|
||||||
@@ -14,9 +16,20 @@
|
|||||||
android:id="@+id/message"
|
android:id="@+id/message"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
android:textColor="?textColorService"
|
android:textColor="?textColorService"
|
||||||
tools:text="Service" />
|
tools:text="Service" />
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/photo"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:src="@tools:sample/backgrounds/scenic"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||||
|
|
||||||
</layout>
|
</layout>
|
||||||
@@ -13,7 +13,9 @@
|
|||||||
|
|
||||||
<action
|
<action
|
||||||
android:id="@+id/toLogin"
|
android:id="@+id/toLogin"
|
||||||
app:destination="@id/loginFragment" />
|
app:destination="@id/loginFragment"
|
||||||
|
app:popUpTo="@id/mainFragment"
|
||||||
|
app:popUpToInclusive="true" />
|
||||||
|
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:id="@+id/nav_graph"
|
android:id="@+id/nav_graph">
|
||||||
app:startDestination="@id/conversationsFragment">
|
|
||||||
|
|
||||||
<include app:graph="@navigation/messages" />
|
<include app:graph="@navigation/messages" />
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,9 @@
|
|||||||
|
|
||||||
<color name="colorSurface">@color/a1_0</color>
|
<color name="colorSurface">@color/a1_0</color>
|
||||||
|
|
||||||
|
<color name="messageOutStrokeColor">@color/n2_100</color>
|
||||||
|
<color name="messageOutColor">@color/n1_10</color>
|
||||||
|
|
||||||
<color name="a1_0">#FFFFFF</color>
|
<color name="a1_0">#FFFFFF</color>
|
||||||
<color name="a1_200">#B1C6FA</color>
|
<color name="a1_200">#B1C6FA</color>
|
||||||
<color name="a1_400">#4184F5</color>
|
<color name="a1_400">#4184F5</color>
|
||||||
|
|||||||
@@ -44,5 +44,11 @@
|
|||||||
<string name="day_short">D</string>
|
<string name="day_short">D</string>
|
||||||
<string name="time_now">Now</string>
|
<string name="time_now">Now</string>
|
||||||
<string name="message_input_hint">Start typing here...</string>
|
<string name="message_input_hint">Start typing here...</string>
|
||||||
|
<string name="input_login_hint">Input login</string>
|
||||||
|
<string name="input_password_hint">Input password</string>
|
||||||
|
<string name="input_code_hint">Input code</string>
|
||||||
|
<string name="validation_required">Validation required</string>
|
||||||
|
<string name="unknown_error_occurred">Unknown error occurred</string>
|
||||||
|
<string name="authorization_failed">Authorization failed</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user