illegal token checking
fixes
This commit is contained in:
@@ -79,6 +79,8 @@ dependencies {
|
||||
|
||||
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("com.google.android.material:material:1.5.0-alpha03")
|
||||
implementation("androidx.core:core-ktx:1.7.0-alpha02")
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:testOnly="false"
|
||||
android:theme="@style/AppTheme">
|
||||
android:theme="@style/AppTheme"
|
||||
tools:replace="android:allowBackup">
|
||||
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
|
||||
@@ -5,7 +5,7 @@ object VKConstants {
|
||||
const val GROUP_FIELDS = "description,members_count,counters,status,verified"
|
||||
|
||||
const val USER_FIELDS =
|
||||
"photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex"
|
||||
"photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex,online_info"
|
||||
|
||||
|
||||
const val API_VERSION = "5.132"
|
||||
|
||||
@@ -3,7 +3,12 @@ package com.meloda.fast.api
|
||||
import org.json.JSONObject
|
||||
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) {
|
||||
|
||||
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.base.BaseVkMessage
|
||||
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 {
|
||||
|
||||
fun isValidationRequired(throwable: Throwable): Boolean {
|
||||
if (throwable !is VKException) return false
|
||||
return throwable.error == VKErrors.NEED_VALIDATION
|
||||
return throwable.error == VkErrors.NEED_VALIDATION
|
||||
}
|
||||
|
||||
fun isCaptchaRequired(throwable: Throwable): Boolean {
|
||||
if (throwable !is VKException) return false
|
||||
return throwable.error == VKErrors.NEED_CAPTCHA
|
||||
return throwable.error == VkErrors.NEED_CAPTCHA
|
||||
}
|
||||
|
||||
fun prepareMessageText(text: String): String {
|
||||
@@ -94,9 +94,7 @@ object VkUtils {
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.STICKER -> {
|
||||
val sticker = baseAttachment.sticker ?: continue
|
||||
attachments += VkSticker(
|
||||
link = sticker.images[0].url
|
||||
)
|
||||
attachments += sticker.asVkSticker()
|
||||
}
|
||||
BaseVkAttachmentItem.AttachmentType.GIFT -> {
|
||||
val gift = baseAttachment.gift ?: continue
|
||||
@@ -275,9 +273,9 @@ object VkUtils {
|
||||
else -> 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 -> {
|
||||
val prefix = when {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.meloda.fast.api.base
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import java.io.IOException
|
||||
import com.meloda.fast.api.VKException
|
||||
|
||||
data class ApiError(
|
||||
@SerializedName("error_code")
|
||||
val errorCode: Int,
|
||||
@SerializedName("error_msg")
|
||||
override var message: String
|
||||
) : IOException()
|
||||
) : VKException(error = message, code = errorCode)
|
||||
|
||||
@@ -33,8 +33,8 @@ data class VkConversation(
|
||||
fun isUser() = type == "user"
|
||||
fun isGroup() = type == "group"
|
||||
|
||||
fun isInUnread() = inRead != lastMessageId
|
||||
fun isOutUnread() = outRead != lastMessageId
|
||||
fun isInUnread() = inRead < lastMessageId
|
||||
fun isOutUnread() = outRead < lastMessageId
|
||||
|
||||
fun isUnread() = isInUnread() || isOutUnread()
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@ data class VkGroup(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val screenName: String,
|
||||
val photo200: String?
|
||||
): Parcelable {
|
||||
val photo200: String?,
|
||||
val membersCount: Int?
|
||||
) : Parcelable {
|
||||
|
||||
override fun toString() = name.trim()
|
||||
|
||||
|
||||
@@ -42,14 +42,31 @@ data class VkMessage(
|
||||
|
||||
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? {
|
||||
if (action == null) return null
|
||||
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,
|
||||
text = text,
|
||||
isOut = isOut,
|
||||
@@ -64,7 +81,10 @@ data class VkMessage(
|
||||
actionMessage = actionMessage,
|
||||
geoType = geoType,
|
||||
important = important
|
||||
)
|
||||
).also {
|
||||
it.attachments = attachments
|
||||
it.forwards = forwards
|
||||
}
|
||||
|
||||
enum class Action(val value: String) {
|
||||
CHAT_CREATE("chat_create"),
|
||||
|
||||
@@ -13,9 +13,13 @@ data class VkUser(
|
||||
val firstName: String,
|
||||
val lastName: String,
|
||||
val online: Boolean,
|
||||
val photo200: String?
|
||||
val photo200: String?,
|
||||
val lastSeen: Int?,
|
||||
val lastSeenStatus: String?
|
||||
) : 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
|
||||
|
||||
import com.meloda.fast.api.model.base.attachments.BaseVkSticker
|
||||
import com.meloda.fast.api.model.base.attachments.StickerSize
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class VkSticker(
|
||||
val link: String
|
||||
) : VkAttachment()
|
||||
val id: Int,
|
||||
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")
|
||||
val photo100: String?,
|
||||
@SerializedName("photo_200")
|
||||
val photo200: String?
|
||||
val photo200: String?,
|
||||
@SerializedName("members_count")
|
||||
val membersCount: Int?
|
||||
) : Parcelable {
|
||||
|
||||
fun asVkGroup() = VkGroup(
|
||||
id = -id,
|
||||
name = name,
|
||||
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,
|
||||
lastName = lastName,
|
||||
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
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.IntDef
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.meloda.fast.api.model.attachments.VkSticker
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@@ -18,6 +20,13 @@ data class BaseVkSticker(
|
||||
val animations: List<Animation>?
|
||||
) : Parcelable {
|
||||
|
||||
fun asVkSticker() = VkSticker(
|
||||
id = stickerId,
|
||||
productId = productId,
|
||||
images = images,
|
||||
backgroundImages = imagesWithBackground
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
data class Image(
|
||||
val width: Int,
|
||||
@@ -31,5 +40,7 @@ data class BaseVkSticker(
|
||||
val url: String
|
||||
) : Parcelable
|
||||
|
||||
|
||||
}
|
||||
|
||||
@IntDef(64, 128, 256, 352)
|
||||
annotation class StickerSize
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.meloda.fast.api.model.request
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@@ -59,7 +58,6 @@ data class MessagesSendRequest(
|
||||
|
||||
@Parcelize
|
||||
data class MessagesMarkAsImportantRequest(
|
||||
@SerializedName("message_ids")
|
||||
val messagesIds: List<Int>,
|
||||
val important: Boolean
|
||||
) : 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
|
||||
|
||||
object ErrorCodes {
|
||||
object VkErrorCodes {
|
||||
const val UNKNOWN_ERROR = 1
|
||||
const val APP_DISABLED = 2
|
||||
const val UNKNOWN_METHOD = 3
|
||||
@@ -41,7 +41,7 @@ object ErrorCodes {
|
||||
const val ACCESS_TO_DOC_DENIED = 1153
|
||||
}
|
||||
|
||||
object VKErrors {
|
||||
object VkErrors {
|
||||
const val UNKNOWN = "unknown_error"
|
||||
|
||||
const val NEED_VALIDATION = "need_validation"
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package com.meloda.fast.api.network
|
||||
|
||||
import com.meloda.fast.api.VKException
|
||||
import com.meloda.fast.api.base.ApiResponse
|
||||
import okhttp3.Request
|
||||
import okio.IOException
|
||||
import okio.Timeout
|
||||
import org.json.JSONObject
|
||||
import retrofit2.*
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
@@ -76,11 +75,16 @@ internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy)
|
||||
) : Callback<T> {
|
||||
|
||||
override fun onResponse(call: Call<T>, response: Response<T>) {
|
||||
val result: Answer<T> = if (response.isSuccessful)
|
||||
Answer.Success(response.body() as T)
|
||||
else Answer.Error(IOException(response.errorBody()?.string() ?: ""))
|
||||
|
||||
if (result is Answer.Error) if (checkErrors(call, result)) return
|
||||
val result: Answer<T> =
|
||||
if (response.isSuccessful) {
|
||||
val baseBody = response.body()
|
||||
if (baseBody !is ApiResponse<*>) Answer.Success(baseBody as T)
|
||||
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))
|
||||
}
|
||||
@@ -91,23 +95,6 @@ internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy)
|
||||
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 {
|
||||
|
||||
@@ -22,6 +22,8 @@ object VKUrls {
|
||||
const val GetHistory = "$API/messages.getHistory"
|
||||
const val Send = "$API/messages.send"
|
||||
const val MarkAsImportant = "$API/messages.markAsImportant"
|
||||
const val GetLongPollServer = "$API/messages.getLongPollServer"
|
||||
const val GetLongPollHistory = "$API/messages.getLongPollHistory"
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.meloda.fast.api.network.datasource
|
||||
|
||||
import com.meloda.fast.api.model.request.MessagesGetHistoryRequest
|
||||
import com.meloda.fast.api.model.request.MessagesGetLongPollServerRequest
|
||||
import com.meloda.fast.api.model.request.MessagesMarkAsImportantRequest
|
||||
import com.meloda.fast.api.model.request.MessagesSendRequest
|
||||
import com.meloda.fast.api.network.repo.MessagesRepo
|
||||
@@ -21,4 +22,7 @@ class MessagesDataSource @Inject constructor(
|
||||
suspend fun markAsImportant(params: MessagesMarkAsImportantRequest) =
|
||||
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
|
||||
|
||||
import com.meloda.fast.api.base.ApiResponse
|
||||
import com.meloda.fast.api.model.base.BaseVkLongPoll
|
||||
import com.meloda.fast.api.model.response.MessagesGetHistoryResponse
|
||||
import com.meloda.fast.api.network.Answer
|
||||
import com.meloda.fast.api.network.VKUrls
|
||||
@@ -22,4 +23,8 @@ interface MessagesRepo {
|
||||
@POST(VKUrls.Messages.MarkAsImportant)
|
||||
suspend fun markAsImportant(@FieldMap params: Map<String, String>): Answer<ApiResponse<List<Int>>>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(VKUrls.Messages.GetLongPollServer)
|
||||
suspend fun getLongPollServer(@FieldMap params: Map<String, String>): Answer<ApiResponse<BaseVkLongPoll>>
|
||||
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
package com.meloda.fast.base
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.LayoutRes
|
||||
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.IllegalTokenEvent
|
||||
import com.meloda.fast.base.viewmodel.VKEvent
|
||||
import kotlinx.coroutines.flow.collect
|
||||
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.viewModelScope
|
||||
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.VKErrors
|
||||
import com.meloda.fast.util.Utils
|
||||
import com.meloda.fast.api.network.VkErrorCodes
|
||||
import com.meloda.fast.api.network.VkErrors
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -26,23 +27,32 @@ abstract class BaseViewModel : ViewModel() {
|
||||
onStart?.invoke()
|
||||
when (val response = job()) {
|
||||
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() } } }
|
||||
|
||||
protected suspend fun <T : VKEvent> sendEvent(event: T) = tasksEventChannel.send(event)
|
||||
|
||||
protected suspend fun checkErrors(throwable: Throwable) {
|
||||
// TODO: 8/31/2021 check illegal token
|
||||
if (throwable is VKException) {
|
||||
private suspend fun checkErrors(throwable: Throwable) {
|
||||
if (throwable is ApiError) {
|
||||
when (throwable.errorCode) {
|
||||
VkErrorCodes.USER_AUTHORIZATION_FAILED -> {
|
||||
sendEvent(IllegalTokenEvent)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if (throwable is VKException) {
|
||||
when (throwable.error) {
|
||||
VKErrors.NEED_CAPTCHA -> {
|
||||
VkErrors.NEED_CAPTCHA -> {
|
||||
throwable.captcha =
|
||||
(throwable.json?.optString("captcha_sid")
|
||||
?: "") to (throwable.json?.optString("captcha_img") ?: "")
|
||||
return
|
||||
}
|
||||
VKErrors.NEED_VALIDATION -> {
|
||||
VkErrors.NEED_VALIDATION -> {
|
||||
throwable.validationSid = throwable.json?.optString("validation_sid")
|
||||
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
|
||||
) : VKEvent()
|
||||
|
||||
data class ErrorEvent(val errorText: String) : VKEvent()
|
||||
|
||||
object IllegalTokenEvent : VKEvent()
|
||||
|
||||
object StartProgressEvent : VKEvent()
|
||||
object StopProgressEvent : VKEvent()
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import com.meloda.fast.database.dao.UsersDao
|
||||
VkUser::class,
|
||||
VkGroup::class
|
||||
],
|
||||
version = 16,
|
||||
version = 18,
|
||||
exportSchema = false
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
@@ -2,16 +2,13 @@ package com.meloda.fast.di
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.meloda.fast.api.network.AuthInterceptor
|
||||
import com.meloda.fast.api.network.ResultCallFactory
|
||||
import com.meloda.fast.api.network.datasource.AuthDataSource
|
||||
import com.meloda.fast.api.network.datasource.ConversationsDataSource
|
||||
import com.meloda.fast.api.network.datasource.MessagesDataSource
|
||||
import com.meloda.fast.api.network.datasource.UsersDataSource
|
||||
import com.meloda.fast.api.network.AuthInterceptor
|
||||
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.api.network.repo.*
|
||||
import com.meloda.fast.database.dao.ConversationsDao
|
||||
import com.meloda.fast.database.dao.MessagesDao
|
||||
import com.meloda.fast.database.dao.UsersDao
|
||||
@@ -81,6 +78,10 @@ object NetworkModule {
|
||||
fun provideMessagesRepo(retrofit: Retrofit): MessagesRepo =
|
||||
retrofit.create(MessagesRepo::class.java)
|
||||
|
||||
@Provides
|
||||
fun provideLongPollRepo(retrofit: Retrofit): LongPollRepo =
|
||||
retrofit.create(LongPollRepo::class.java)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAuthDataSource(
|
||||
|
||||
@@ -88,9 +88,10 @@ class ConversationsAdapter constructor(
|
||||
}
|
||||
|
||||
binding.avatar.isVisible = avatar != null
|
||||
binding.avatarPlaceholder.isVisible = avatar == null
|
||||
|
||||
if (avatar == null) {
|
||||
binding.avatarPlaceholder.isVisible = true
|
||||
|
||||
if (conversation.ownerId == VKConstants.FAST_GROUP_ID) {
|
||||
binding.placeholderBack.setImageDrawable(
|
||||
ColorDrawable(
|
||||
@@ -114,7 +115,13 @@ class ConversationsAdapter constructor(
|
||||
binding.avatar.setImageDrawable(null)
|
||||
}
|
||||
} 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
|
||||
@@ -155,7 +162,8 @@ class ConversationsAdapter constructor(
|
||||
message = message
|
||||
) else null
|
||||
|
||||
val messageText = (if (actionMessage != null ||
|
||||
val messageText = (if (
|
||||
actionMessage != null ||
|
||||
forwardsMessage != null ||
|
||||
attachmentText != null
|
||||
) ""
|
||||
|
||||
@@ -45,6 +45,7 @@ class ConversationsFragment :
|
||||
}
|
||||
|
||||
private var isPaused = false
|
||||
private var isExpanded = true
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
@@ -63,7 +64,9 @@ class ConversationsFragment :
|
||||
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) {
|
||||
binding.avatarContainer.alpha = 0f
|
||||
return@OnOffsetChangedListener
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.viewbinding.library.fragment.viewBinding
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
@@ -48,6 +49,11 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
||||
private var captchaInputLayout: 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?) {
|
||||
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(
|
||||
loginString: String?,
|
||||
passwordString: String?,
|
||||
@@ -167,22 +172,22 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
||||
|
||||
if (loginString?.isEmpty() == true) {
|
||||
isValidated = false
|
||||
setError("Input login", binding.loginLayout)
|
||||
setError(getString(R.string.input_login_hint), binding.loginLayout)
|
||||
}
|
||||
|
||||
if (passwordString?.isEmpty() == true) {
|
||||
isValidated = false
|
||||
setError("Input password", binding.passwordLayout)
|
||||
setError(getString(R.string.input_password_hint), binding.passwordLayout)
|
||||
}
|
||||
|
||||
if (captchaCode?.isEmpty() == true && captchaInputLayout != null) {
|
||||
isValidated = false
|
||||
setError("Input code", captchaInputLayout!!)
|
||||
setError(getString(R.string.input_code_hint), captchaInputLayout!!)
|
||||
}
|
||||
|
||||
if (validationCode?.isEmpty() == true && validationInputLayout != null) {
|
||||
isValidated = false
|
||||
setError("Input code", validationInputLayout!!)
|
||||
setError(getString(R.string.input_code_hint), validationInputLayout!!)
|
||||
}
|
||||
|
||||
return isValidated
|
||||
@@ -281,9 +286,8 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
|
||||
validationBinding.cancel.setOnClickListener { dialog.dismiss() }
|
||||
}
|
||||
|
||||
// TODO: 8/31/2021 show snackbar
|
||||
private fun showValidationRequired() {
|
||||
|
||||
Toast.makeText(requireContext(), R.string.validation_required, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
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.VKException
|
||||
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.base.viewmodel.BaseViewModel
|
||||
import com.meloda.fast.base.viewmodel.StartProgressEvent
|
||||
import com.meloda.fast.base.viewmodel.StopProgressEvent
|
||||
import com.meloda.fast.base.viewmodel.VKEvent
|
||||
import com.meloda.fast.api.network.datasource.AuthDataSource
|
||||
import com.meloda.fast.base.viewmodel.*
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
@@ -21,6 +18,8 @@ class LoginViewModel @Inject constructor(
|
||||
private val dataSource: AuthDataSource
|
||||
) : BaseViewModel() {
|
||||
|
||||
lateinit var unknownErrorDefaultText: String
|
||||
|
||||
fun login(
|
||||
login: String,
|
||||
password: String,
|
||||
@@ -45,8 +44,8 @@ class LoginViewModel @Inject constructor(
|
||||
)
|
||||
},
|
||||
onAnswer = {
|
||||
// TODO: 8/31/2021 do something
|
||||
if (it.userId == null || it.accessToken == null) {
|
||||
sendEvent(ErrorEvent(unknownErrorDefaultText))
|
||||
return@makeJob
|
||||
}
|
||||
|
||||
@@ -56,7 +55,6 @@ class LoginViewModel @Inject constructor(
|
||||
sendEvent(SuccessAuth(haveAuthorized = true))
|
||||
},
|
||||
onError = {
|
||||
checkErrors(it)
|
||||
if (it !is VKException) return@makeJob
|
||||
|
||||
twoFaCode?.let { sendEvent(CodeSent) }
|
||||
|
||||
@@ -21,9 +21,8 @@ class MainFragment : BaseViewModelFragment<MainViewModel>(R.layout.fragment_main
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
if (savedInstanceState == null) setupBottomBar()
|
||||
|
||||
if (!UserConfig.isLoggedIn()) findNavController().navigate(R.id.toLogin)
|
||||
else if (savedInstanceState == null) setupBottomBar()
|
||||
}
|
||||
|
||||
private fun setupBottomBar() {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package com.meloda.fast.screens.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.LinearLayoutCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
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.VkUser
|
||||
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.BaseHolder
|
||||
import com.meloda.fast.common.AppGlobal
|
||||
import com.meloda.fast.databinding.*
|
||||
import com.meloda.fast.util.AndroidUtils
|
||||
import kotlin.math.roundToInt
|
||||
@@ -31,30 +35,31 @@ class MessagesHistoryAdapter constructor(
|
||||
) : BaseAdapter<VkMessage, MessagesHistoryAdapter.Holder>(context, values, COMPARATOR) {
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
var viewType: Int = when {
|
||||
isPositionHeader(position) -> HEADER
|
||||
isPositionFooter(position) -> FOOTER
|
||||
else -> -1
|
||||
when {
|
||||
isPositionHeader(position) -> return HEADER
|
||||
isPositionFooter(position) -> return FOOTER
|
||||
}
|
||||
|
||||
if (viewType == -1) {
|
||||
getItem(position).let {
|
||||
if (it.action != null) viewType = SERVICE
|
||||
getItem(position).let { message ->
|
||||
if (message.action != null) return SERVICE
|
||||
|
||||
val attachments = it.attachments ?: return@let
|
||||
if (attachments.isEmpty()) return@let
|
||||
if (!message.attachments.isNullOrEmpty()) {
|
||||
val attachments = message.attachments ?: return@let
|
||||
if (VkUtils.isAttachmentsHaveOneType(attachments) &&
|
||||
attachments[0] is VkPhoto
|
||||
) {
|
||||
return if (it.isOut) ATTACHMENT_PHOTOS_OUT else ATTACHMENT_PHOTOS_IN
|
||||
) return if (message.isOut) ATTACHMENT_PHOTOS_OUT
|
||||
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 (!it.isOut) viewType = INCOMING
|
||||
}
|
||||
if (message.isOut) return OUTGOING
|
||||
if (!message.isOut) return INCOMING
|
||||
}
|
||||
|
||||
return viewType
|
||||
return -1
|
||||
}
|
||||
|
||||
private fun isPositionHeader(position: Int) = position == 0
|
||||
@@ -67,11 +72,17 @@ class MessagesHistoryAdapter constructor(
|
||||
SERVICE -> ServiceMessage(
|
||||
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(
|
||||
ItemMessageAttachmentPhotoInBinding.inflate(inflater, parent, false)
|
||||
ItemMessageAttachmentPhotosInBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
ATTACHMENT_PHOTOS_OUT -> AttachmentPhotosOutgoing(
|
||||
ItemMessageAttachmentPhotoOutBinding.inflate(inflater, parent, false)
|
||||
ItemMessageAttachmentPhotosOutBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
OUTGOING -> OutgoingMessage(
|
||||
ItemMessageOutBinding.inflate(inflater, parent, false)
|
||||
@@ -79,7 +90,7 @@ class MessagesHistoryAdapter constructor(
|
||||
INCOMING -> IncomingMessage(
|
||||
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 AttachmentPhotosIncoming(
|
||||
private val binding: ItemMessageAttachmentPhotoInBinding
|
||||
inner class IncomingMessage(
|
||||
private val binding: ItemMessageInBinding
|
||||
) : 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 {
|
||||
binding.photo.shapeAppearanceModel = binding.photo.shapeAppearanceModel.withCornerSize {
|
||||
AndroidUtils.px(12)
|
||||
}
|
||||
MessagesManager.setRootMaxWidth(binding.bubble)
|
||||
}
|
||||
|
||||
override fun bind(position: Int) {
|
||||
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(
|
||||
AndroidUtils.px(size.width).roundToInt(),
|
||||
AndroidUtils.px(size.height).roundToInt()
|
||||
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
|
||||
|
||||
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(
|
||||
private val binding: ItemMessageAttachmentPhotoOutBinding
|
||||
MessagesManager.setMessageText(
|
||||
message = message,
|
||||
textView = binding.text
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
inner class OutgoingMessage(
|
||||
private val binding: ItemMessageOutBinding
|
||||
) : 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 {
|
||||
binding.photo.shapeAppearanceModel = binding.photo.shapeAppearanceModel.withCornerSize {
|
||||
AndroidUtils.px(12)
|
||||
}
|
||||
MessagesManager.setRootMaxWidth(binding.bubble)
|
||||
}
|
||||
|
||||
override fun bind(position: Int) {
|
||||
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(
|
||||
AndroidUtils.px(size.width).roundToInt(),
|
||||
AndroidUtils.px(size.height).roundToInt()
|
||||
)
|
||||
binding.unread.isVisible = message.isRead(conversation)
|
||||
|
||||
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)
|
||||
|
||||
init {
|
||||
binding.photo.shapeAppearanceModel.run {
|
||||
withCornerSize { AndroidUtils.px(4) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun bind(position: Int) {
|
||||
val message = getItem(position)
|
||||
|
||||
@@ -188,42 +275,104 @@ class MessagesHistoryAdapter constructor(
|
||||
messageUser = messageUser,
|
||||
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(
|
||||
private val binding: ItemMessageOutBinding
|
||||
inner class AttachmentPhotosIncoming(
|
||||
private val binding: ItemMessageAttachmentPhotosInBinding
|
||||
) : Holder(binding.root) {
|
||||
|
||||
init {
|
||||
binding.bubble.maxWidth = (AppGlobal.screenWidth * 0.75).roundToInt()
|
||||
}
|
||||
|
||||
override fun bind(position: Int) {
|
||||
val message = getItem(position)
|
||||
|
||||
binding.text.text = message.text ?: "[no_message]"
|
||||
|
||||
binding.unread.isVisible = message.isRead(conversation)
|
||||
MessagesManager.loadPhotos(
|
||||
context = context,
|
||||
message = message,
|
||||
binding.photosContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
inner class IncomingMessage(
|
||||
private val binding: ItemMessageInBinding
|
||||
inner class AttachmentPhotosOutgoing(
|
||||
private val binding: ItemMessageAttachmentPhotosOutBinding
|
||||
) : Holder(binding.root) {
|
||||
|
||||
init {
|
||||
binding.bubble.maxWidth = (AppGlobal.screenWidth * 0.7).roundToInt()
|
||||
}
|
||||
|
||||
override fun bind(position: Int) {
|
||||
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 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()
|
||||
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
|
||||
else if (nextMessage.date - message.date >= 60) View.VISIBLE
|
||||
else View.INVISIBLE
|
||||
}
|
||||
|
||||
val messageUser: VkUser? = if (message.isUser()) {
|
||||
profiles[message.fromId]
|
||||
@@ -246,24 +396,34 @@ class MessagesHistoryAdapter constructor(
|
||||
else -> null
|
||||
}
|
||||
|
||||
binding.avatar.load(avatar) { crossfade(100) }
|
||||
|
||||
val title = when {
|
||||
message.isUser() && messageUser != null -> messageUser.firstName
|
||||
message.isUser() && messageUser != null -> messageUser.fullName
|
||||
message.isGroup() && messageGroup != null -> messageGroup.name
|
||||
else -> null
|
||||
}
|
||||
|
||||
binding.avatar.load(avatar) { crossfade(100) }
|
||||
|
||||
binding.text.text = message.text ?: "[no_message]"
|
||||
|
||||
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
|
||||
binding.avatar.setOnLongClickListener {
|
||||
Toast.makeText(context, title, Toast.LENGTH_SHORT).apply {
|
||||
setGravity(
|
||||
Gravity.START or Gravity.BOTTOM,
|
||||
0,
|
||||
-50
|
||||
)
|
||||
}.show()
|
||||
true
|
||||
}
|
||||
|
||||
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 OUTGOING = 4
|
||||
|
||||
|
||||
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>() {
|
||||
override fun areItemsTheSame(
|
||||
|
||||
@@ -80,8 +80,15 @@ class MessagesHistoryFragment :
|
||||
|
||||
val status = when {
|
||||
conversation.isChat() -> "${conversation.membersCount} members"
|
||||
conversation.isUser() -> if (user?.online == true) "Online" else "Last seen at [...]"
|
||||
conversation.isGroup() -> "[Group status]"
|
||||
conversation.isUser() -> when {
|
||||
// TODO: 9/15/2021 user normal time
|
||||
user?.online == true -> "Online"
|
||||
user?.lastSeen != null -> "Last seen at ${
|
||||
SimpleDateFormat("HH:mm", Locale.getDefault()).format(user?.lastSeen!! * 1000L)
|
||||
}"
|
||||
else -> if (user?.lastSeenStatus != null) "Last seen ${user?.lastSeenStatus!!}" else "Last seen recently"
|
||||
}
|
||||
conversation.isGroup() -> if (group?.membersCount != null) "${group?.membersCount} members" else "Group"
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -162,7 +169,6 @@ class MessagesHistoryFragment :
|
||||
}
|
||||
|
||||
action.observe(viewLifecycleOwner) {
|
||||
|
||||
binding.action.animate()
|
||||
.scaleX(1.25f)
|
||||
.scaleY(1.25f)
|
||||
@@ -216,18 +222,19 @@ class MessagesHistoryFragment :
|
||||
peerId = conversation.id,
|
||||
message = messageText,
|
||||
randomId = 0
|
||||
) { message = message.changeId(it) }
|
||||
) { message = message.copyMessage(id = it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEvent(event: VKEvent) {
|
||||
super.onEvent(event)
|
||||
|
||||
when (event) {
|
||||
is MessagesMarkAsImportant -> markMessagesAsImportant(event)
|
||||
is MessagesLoaded -> refreshMessages(event)
|
||||
is StartProgressEvent -> onProgressStarted()
|
||||
is StopProgressEvent -> onProgressStopped()
|
||||
}
|
||||
super.onEvent(event)
|
||||
}
|
||||
|
||||
private fun onProgressStarted() {
|
||||
@@ -276,7 +283,9 @@ class MessagesHistoryFragment :
|
||||
val message = adapter.values[i]
|
||||
if (event.messagesIds.contains(message.id)) {
|
||||
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) {
|
||||
val message = adapter.values[position]
|
||||
if (message.action != null) return
|
||||
|
||||
val important = if (message.important) "Unmark as important" else "Mark as important"
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.meloda.fast.screens.messages
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.meloda.fast.api.VKConstants
|
||||
import com.meloda.fast.api.model.VkConversation
|
||||
import com.meloda.fast.api.model.VkGroup
|
||||
import com.meloda.fast.api.model.VkMessage
|
||||
@@ -28,10 +29,10 @@ class MessagesHistoryViewModel @Inject constructor(
|
||||
makeJob({
|
||||
dataSource.getHistory(
|
||||
MessagesGetHistoryRequest(
|
||||
count = 90,
|
||||
count = 30,
|
||||
peerId = peerId,
|
||||
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 androidx.appcompat.widget.AppCompatImageView
|
||||
|
||||
// TODO: 8/31/2021 extend ShapeableImageView and set corners for half of size
|
||||
class CircleImageView : AppCompatImageView {
|
||||
|
||||
companion object {
|
||||
@@ -27,7 +26,6 @@ class CircleImageView : AppCompatImageView {
|
||||
attrs,
|
||||
defStyleAttr
|
||||
) {
|
||||
|
||||
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"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="@android:color/white" />
|
||||
<solid android:color="@color/messageOutColor" />
|
||||
|
||||
<corners
|
||||
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">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:id="@+id/collapsingToolbarLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:elevation="0dp"
|
||||
app:collapsedTitleTextAppearance="@style/CollapsingToolbarCollapsedTitle"
|
||||
app:expandedTitleTextAppearance="@style/CollapsingToolbarTitle"
|
||||
app:layout_scrollFlags="scroll|enterAlways|snap"
|
||||
app:layout_scrollFlags="scroll|enterAlwaysCollapsed|snap"
|
||||
app:title="Messages">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
|
||||
@@ -34,8 +34,7 @@
|
||||
<FrameLayout
|
||||
android:id="@+id/avatarPlaceholder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
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"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<FrameLayout
|
||||
<com.meloda.fast.widget.BoundedFrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="12dp">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/photo"
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/photosContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/backgrounds/scenic" />
|
||||
android:layout_gravity="end"
|
||||
android:orientation="vertical" />
|
||||
|
||||
</FrameLayout>
|
||||
</com.meloda.fast.widget.BoundedFrameLayout>
|
||||
|
||||
</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: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"
|
||||
@@ -32,6 +38,12 @@
|
||||
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"
|
||||
@@ -51,9 +63,15 @@
|
||||
|
||||
</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>
|
||||
</layout>
|
||||
@@ -6,6 +6,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end|bottom"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingVertical="2.5dp">
|
||||
|
||||
@@ -17,12 +18,22 @@
|
||||
android:layout_marginBottom="20dp"
|
||||
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
|
||||
android:id="@+id/bubbleStroke"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/ic_message_out_background"
|
||||
android:backgroundTint="@color/n2_100"
|
||||
android:background="@drawable/ic_message_out_background_stroke"
|
||||
android:padding="1.5dp"
|
||||
tools:ignore="UselessParent">
|
||||
|
||||
@@ -31,8 +42,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/ic_message_out_background"
|
||||
android:backgroundTint="@color/n1_10">
|
||||
android:background="@drawable/ic_message_out_background">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/text"
|
||||
@@ -40,7 +50,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical|start"
|
||||
android:padding="15dp"
|
||||
android:textColor="@color/n1_800"
|
||||
android:textColor="@color/n1_900"
|
||||
tools:text="This is test" />
|
||||
|
||||
</com.meloda.fast.widget.BoundedFrameLayout>
|
||||
@@ -49,4 +59,6 @@
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
</layout>
|
||||
@@ -5,6 +5,8 @@
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="12dp"
|
||||
@@ -14,9 +16,20 @@
|
||||
android:id="@+id/message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textColor="?textColorService"
|
||||
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>
|
||||
|
||||
</layout>
|
||||
@@ -13,7 +13,9 @@
|
||||
|
||||
<action
|
||||
android:id="@+id/toLogin"
|
||||
app:destination="@id/loginFragment" />
|
||||
app:destination="@id/loginFragment"
|
||||
app:popUpTo="@id/mainFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
</fragment>
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/nav_graph"
|
||||
app:startDestination="@id/conversationsFragment">
|
||||
android:id="@+id/nav_graph">
|
||||
|
||||
<include app:graph="@navigation/messages" />
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
|
||||
<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_200">#B1C6FA</color>
|
||||
<color name="a1_400">#4184F5</color>
|
||||
|
||||
@@ -44,5 +44,11 @@
|
||||
<string name="day_short">D</string>
|
||||
<string name="time_now">Now</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>
|
||||
|
||||
Reference in New Issue
Block a user