illegal token checking

fixes
This commit is contained in:
2021-09-17 16:25:26 +03:00
parent a3a282c32c
commit d1ed98691c
57 changed files with 968 additions and 251 deletions
+2
View File
@@ -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")
+2 -1
View File
@@ -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,8 +12,9 @@ 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?,
): Parcelable { val membersCount: Int?
) : 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 {
@@ -70,4 +68,17 @@ data class MessagesMarkAsImportantRequest(
"important" to (if (important) 1 else 0).toString() "important" to (if (important) 1 else 0).toString()
) )
}
@Parcelize
data class MessagesGetLongPollServerRequest(
val needPts: Boolean,
val version: Int
) : Parcelable {
val map
get() = mutableMapOf(
"need_pts" to (if (needPts) 1 else 0).toString(),
"version" to version.toString()
)
} }
@@ -1,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 (it.isOut) viewType = OUTGOING
if (!it.isOut) viewType = INCOMING if (attachments[0] is VkSticker) return if (message.isOut) ATTACHMENT_STICKER_OUT
else ATTACHMENT_STICKER_IN
} }
if (message.isOut) return OUTGOING
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
}
MessagesManager.setMessageText(
message = message,
textView = binding.text
)
}
} }
inner class AttachmentPhotosOutgoing( inner class OutgoingMessage(
private val binding: ItemMessageAttachmentPhotoOutBinding 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,49 +275,112 @@ 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 IncomingMessage( inner class AttachmentPhotosOutgoing(
private val binding: ItemMessageInBinding private val binding: ItemMessageAttachmentPhotosOutBinding
) : 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()) {
if (prevMessage == null || prevMessage.fromId != message.fromId) message.isPeerChat() binding.spacer.isVisible =
else message.date - prevMessage.date >= 60 !(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
}
binding.avatar.visibility = val sticker = message.attachments?.get(0) as? VkSticker ?: return
if (nextMessage == null || nextMessage.fromId != message.fromId) if (message.isPeerChat()) View.VISIBLE else View.GONE val url = sticker.urlForSize(352)!!
else if (nextMessage.date - message.date >= 60) View.VISIBLE
else View.INVISIBLE 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
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()) { 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>
@@ -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>
+32 -14
View File
@@ -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,28 +38,40 @@
android:textColor="@color/a3_700" android:textColor="@color/a3_700"
tools:text="@tools:sample/full_names" /> tools:text="@tools:sample/full_names" />
<com.meloda.fast.widget.BoundedFrameLayout <androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/bubble"
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_in_background" android:gravity="bottom"
android:backgroundTint="@color/n2_100" android:orientation="horizontal">
tools:ignore="UselessParent">
<com.google.android.material.textview.MaterialTextView <com.meloda.fast.widget.BoundedFrameLayout
android:id="@+id/text" android:id="@+id/bubble"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:background="@drawable/ic_message_in_background"
android:padding="15dp" android:backgroundTint="@color/n2_100"
android:textColor="@color/n1_800" tools:ignore="UselessParent">
tools:text="This" />
</com.meloda.fast.widget.BoundedFrameLayout> <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> </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</layout> </layout>
+31 -19
View File
@@ -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,35 +18,46 @@
android:layout_marginBottom="20dp" android:layout_marginBottom="20dp"
android:src="@color/a3_200" /> android:src="@color/a3_200" />
<FrameLayout <androidx.appcompat.widget.LinearLayoutCompat
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:orientation="vertical">
android:backgroundTint="@color/n2_100"
android:padding="1.5dp"
tools:ignore="UselessParent">
<com.meloda.fast.widget.BoundedFrameLayout <Space
android:id="@+id/bubble" 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_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:background="@drawable/ic_message_out_background_stroke"
android:background="@drawable/ic_message_out_background" android:padding="1.5dp"
android:backgroundTint="@color/n1_10"> tools:ignore="UselessParent">
<com.google.android.material.textview.MaterialTextView <com.meloda.fast.widget.BoundedFrameLayout
android:id="@+id/text" android:id="@+id/bubble"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start" android:layout_gravity="center"
android:padding="15dp" android:background="@drawable/ic_message_out_background">
android:textColor="@color/n1_800"
tools:text="This is test" />
</com.meloda.fast.widget.BoundedFrameLayout> <com.google.android.material.textview.MaterialTextView
</FrameLayout> android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
android:padding="15dp"
android:textColor="@color/n1_900"
tools:text="This is test" />
</com.meloda.fast.widget.BoundedFrameLayout>
</FrameLayout>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
@@ -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>
+3 -1
View File
@@ -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 -2
View File
@@ -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" />
+3
View File
@@ -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>
+6
View File
@@ -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>