forked from melod1n/fast-messenger
Simple chat & small fixes
This commit is contained in:
@@ -113,7 +113,7 @@ dependencies {
|
||||
kapt("com.google.dagger:hilt-android-compiler:2.38.1")
|
||||
implementation("androidx.hilt:hilt-navigation-fragment:1.0.0")
|
||||
|
||||
implementation("com.github.yogacp:android-viewbinding:1.0.2")
|
||||
implementation("com.github.yogacp:android-viewbinding:1.0.3")
|
||||
|
||||
implementation("io.coil-kt:coil:1.3.2")
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ object VKConstants {
|
||||
const val VK_APP_ID = "2274003"
|
||||
const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH"
|
||||
|
||||
const val FAST_GROUP_ID = -119516304
|
||||
|
||||
object Auth {
|
||||
const val SCOPE = "notify," +
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.graphics.drawable.Drawable
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.api.model.VkGroup
|
||||
import com.meloda.fast.api.model.VkGroupCall
|
||||
import com.meloda.fast.api.model.VkMessage
|
||||
import com.meloda.fast.api.model.VkUser
|
||||
import com.meloda.fast.api.model.attachments.*
|
||||
@@ -25,6 +24,12 @@ object VkUtils {
|
||||
return throwable.error == VKErrors.NEED_CAPTCHA
|
||||
}
|
||||
|
||||
fun prepareMessageText(text: String): String {
|
||||
return text
|
||||
.replace("\n", " ")
|
||||
.replace("&", "&")
|
||||
}
|
||||
|
||||
fun parseForwards(baseForwards: List<BaseVkMessage>?): List<VkMessage>? {
|
||||
if (baseForwards.isNullOrEmpty()) return null
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.meloda.fast.api.datasource
|
||||
|
||||
import com.meloda.fast.api.network.repo.MessagesRepo
|
||||
import com.meloda.fast.api.network.request.MessagesGetHistoryRequest
|
||||
import com.meloda.fast.api.network.request.MessagesSendRequest
|
||||
import com.meloda.fast.database.dao.MessagesDao
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -9,4 +11,8 @@ class MessagesDataSource @Inject constructor(
|
||||
private val dao: MessagesDao
|
||||
) {
|
||||
|
||||
suspend fun getHistory(params: MessagesGetHistoryRequest) = repo.getHistory(params.map)
|
||||
|
||||
suspend fun send(params: MessagesSendRequest) = repo.send(params.map)
|
||||
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Entity(tableName = "conversations")
|
||||
@Parcelize
|
||||
data class VkConversation(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
val id: Int,
|
||||
@@ -18,8 +21,9 @@ data class VkConversation(
|
||||
val outRead: Int,
|
||||
val isMarkedUnread: Boolean,
|
||||
val lastMessageId: Int,
|
||||
val unreadCount: Int?
|
||||
) {
|
||||
val unreadCount: Int?,
|
||||
val membersCount: Int?
|
||||
) : Parcelable {
|
||||
@Ignore
|
||||
var lastMessage: VkMessage? = null
|
||||
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Entity(tableName = "groups")
|
||||
@Parcelize
|
||||
data class VkGroup(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val screenName: String,
|
||||
val photo200: String?
|
||||
) {
|
||||
): Parcelable {
|
||||
|
||||
override fun toString() = name.trim()
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import com.meloda.fast.api.model.attachments.VkAttachment
|
||||
|
||||
data class VkGroupCall(
|
||||
val initiatorId: Int
|
||||
) : VkAttachment()
|
||||
@@ -1,41 +1,69 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.PrimaryKey
|
||||
import com.meloda.fast.api.model.attachments.VkAttachment
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Entity(tableName = "messages")
|
||||
@Parcelize
|
||||
data class VkMessage(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
val id: Int,
|
||||
val text: String?,
|
||||
val text: String? = null,
|
||||
val isOut: Boolean,
|
||||
val peerId: Int,
|
||||
val fromId: Int,
|
||||
val date: Int,
|
||||
val action: String?,
|
||||
val actionMemberId: Int?,
|
||||
val actionText: String?,
|
||||
val actionConversationMessageId: Int?,
|
||||
val actionMessage: String?,
|
||||
val geoType: String?
|
||||
) {
|
||||
val randomId: Int,
|
||||
val action: String? = null,
|
||||
val actionMemberId: Int? = null,
|
||||
val actionText: String? = null,
|
||||
val actionConversationMessageId: Int? = null,
|
||||
val actionMessage: String? = null,
|
||||
val geoType: String? = null
|
||||
) : Parcelable {
|
||||
|
||||
@IgnoredOnParcel
|
||||
@Ignore
|
||||
var forwards: List<VkMessage>? = null
|
||||
|
||||
@IgnoredOnParcel
|
||||
@Ignore
|
||||
var attachments: List<VkAttachment>? = null
|
||||
|
||||
fun isPeerChat() = peerId > 2_000_000_000
|
||||
|
||||
fun isUser() = fromId > 0
|
||||
|
||||
fun isGroup() = fromId < 0
|
||||
|
||||
fun isRead(conversation: VkConversation) = conversation.outRead < id
|
||||
|
||||
fun getPreparedAction(): Action? {
|
||||
if (action == null) return null
|
||||
return Action.parse(action)
|
||||
}
|
||||
|
||||
fun changeId(id: Int) = VkMessage(
|
||||
id = id,
|
||||
text = text,
|
||||
isOut = isOut,
|
||||
peerId = peerId,
|
||||
fromId = fromId,
|
||||
date = date,
|
||||
randomId = randomId,
|
||||
action = action,
|
||||
actionMemberId = actionMemberId,
|
||||
actionText = actionText,
|
||||
actionConversationMessageId = actionConversationMessageId,
|
||||
actionMessage = actionMessage,
|
||||
geoType = geoType
|
||||
)
|
||||
|
||||
enum class Action(val value: String) {
|
||||
CHAT_CREATE("chat_create"),
|
||||
CHAT_PHOTO_UPDATE("chat_photo_update"),
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package com.meloda.fast.api.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Entity(tableName = "users")
|
||||
@Parcelize
|
||||
data class VkUser(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
val id: Int,
|
||||
@@ -11,7 +14,7 @@ data class VkUser(
|
||||
val lastName: String,
|
||||
val online: Boolean,
|
||||
val photo200: String?
|
||||
) {
|
||||
) : Parcelable {
|
||||
|
||||
override fun toString() = "$firstName $lastName".trim()
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.meloda.fast.api.model.attachments
|
||||
|
||||
data class VkGroupCall(
|
||||
val initiatorId: Int
|
||||
) : VkAttachment()
|
||||
@@ -50,7 +50,8 @@ data class BaseVkConversation(
|
||||
outRead = outRead,
|
||||
isMarkedUnread = isMarkedUnread,
|
||||
lastMessageId = lastMessageId,
|
||||
unreadCount = unreadCount
|
||||
unreadCount = unreadCount,
|
||||
membersCount = chatSettings?.membersCount
|
||||
).apply { this.lastMessage = lastMessage }
|
||||
|
||||
@Parcelize
|
||||
|
||||
@@ -40,6 +40,7 @@ data class BaseVkMessage(
|
||||
peerId = peerId,
|
||||
fromId = fromId,
|
||||
date = date,
|
||||
randomId = randomId,
|
||||
action = action?.type,
|
||||
actionMemberId = action?.memberId,
|
||||
actionText = action?.text,
|
||||
|
||||
@@ -6,16 +6,21 @@ object VKUrls {
|
||||
const val API = "https://api.vk.com/method"
|
||||
|
||||
object Auth {
|
||||
const val directAuth = "$OAUTH/token"
|
||||
const val sendSms = "$API/auth.validatePhone"
|
||||
const val DirectAuth = "$OAUTH/token"
|
||||
const val SendSms = "$API/auth.validatePhone"
|
||||
}
|
||||
|
||||
object Conversations {
|
||||
const val get = "$API/messages.getConversations"
|
||||
const val Get = "$API/messages.getConversations"
|
||||
}
|
||||
|
||||
object Users {
|
||||
const val getById = "$API/users.get"
|
||||
const val GetById = "$API/users.get"
|
||||
}
|
||||
|
||||
object Messages {
|
||||
const val GetHistory = "$API/messages.getHistory"
|
||||
const val Send = "$API/messages.send"
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ import retrofit2.http.*
|
||||
|
||||
interface AuthRepo {
|
||||
|
||||
@GET(VKUrls.Auth.directAuth)
|
||||
@GET(VKUrls.Auth.DirectAuth)
|
||||
suspend fun auth(@QueryMap param: Map<String, String?>): Answer<ResponseAuthDirect>
|
||||
|
||||
@GET(VKUrls.Auth.sendSms)
|
||||
@GET(VKUrls.Auth.SendSms)
|
||||
suspend fun sendSms(@Query("sid") validationSid: String): Answer<ResponseSendSms>
|
||||
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import retrofit2.http.POST
|
||||
interface ConversationsRepo {
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(VKUrls.Conversations.get)
|
||||
@POST(VKUrls.Conversations.Get)
|
||||
suspend fun getAllChats(@FieldMap params: Map<String, String>): Answer<ApiResponse<ConversationsGetResponse>>
|
||||
|
||||
}
|
||||
@@ -1,4 +1,21 @@
|
||||
package com.meloda.fast.api.network.repo
|
||||
|
||||
import com.meloda.fast.api.base.ApiResponse
|
||||
import com.meloda.fast.api.network.Answer
|
||||
import com.meloda.fast.api.network.VKUrls
|
||||
import com.meloda.fast.api.network.response.MessagesGetHistoryResponse
|
||||
import retrofit2.http.FieldMap
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface MessagesRepo {
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(VKUrls.Messages.GetHistory)
|
||||
suspend fun getHistory(@FieldMap params: Map<String, String>): Answer<ApiResponse<MessagesGetHistoryResponse>>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(VKUrls.Messages.Send)
|
||||
suspend fun send(@FieldMap params: Map<String, String>): Answer<ApiResponse<Int>>
|
||||
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import retrofit2.http.POST
|
||||
interface UsersRepo {
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(VKUrls.Users.getById)
|
||||
@POST(VKUrls.Users.GetById)
|
||||
suspend fun getById(
|
||||
@FieldMap params: Map<String, String>?
|
||||
): Answer<ApiResponse<List<BaseVkUser>>>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.meloda.fast.api.network.request
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@@ -11,7 +10,6 @@ data class ConversationsGetRequest(
|
||||
val fields: String = "",
|
||||
val filter: String = "all",
|
||||
val extended: Boolean? = true,
|
||||
@SerializedName("start_message_id")
|
||||
val startMessageId: Int? = null
|
||||
) : Parcelable {
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.meloda.fast.api.network.request
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class MessagesGetHistoryRequest(
|
||||
val count: Int? = null,
|
||||
val offset: Int? = null,
|
||||
val peerId: Int,
|
||||
val extended: Boolean? = null,
|
||||
val startMessageId: Int? = null,
|
||||
val rev: Boolean? = null,
|
||||
val fields: String? = null,
|
||||
) : Parcelable {
|
||||
|
||||
val map
|
||||
get() = mutableMapOf(
|
||||
"peer_id" to peerId.toString()
|
||||
).apply {
|
||||
count?.let { this["count"] = it.toString() }
|
||||
offset?.let { this["offset"] = it.toString() }
|
||||
extended?.let { this["extended"] = (if (it) 1 else 0).toString() }
|
||||
startMessageId?.let { this["start_message_id"] = it.toString() }
|
||||
rev?.let { this["rev"] = (if (it) 1 else 0).toString() }
|
||||
fields?.let { this["fields"] = it }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class MessagesSendRequest(
|
||||
val peerId: Int,
|
||||
val randomId: Int = 0,
|
||||
val message: String? = null,
|
||||
val lat: Int? = null,
|
||||
val lon: Int? = null,
|
||||
val replyTo: Int? = null,
|
||||
val stickerId: Int? = null,
|
||||
val disableMentions: Boolean? = null,
|
||||
val dontParseLinks: Boolean? = null
|
||||
) : Parcelable {
|
||||
|
||||
val map
|
||||
get() = mutableMapOf(
|
||||
"peer_id" to peerId.toString(),
|
||||
"random_id" to randomId.toString()
|
||||
).apply {
|
||||
message?.let { this["message"] = it }
|
||||
lat?.let { this["lat"] = it.toString() }
|
||||
lon?.let { this["lon"] = it.toString() }
|
||||
replyTo?.let { this["reply_to"] = it.toString() }
|
||||
stickerId?.let { this["sticker_id"] = it.toString() }
|
||||
disableMentions?.let { this["disable_mentions"] = (if (it) 1 else 0).toString() }
|
||||
dontParseLinks?.let { this["dont_parse_links"] = (if (it) 1 else 0).toString() }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.meloda.fast.api.network.response
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.meloda.fast.api.model.base.BaseVkConversation
|
||||
import com.meloda.fast.api.model.base.BaseVkGroup
|
||||
import com.meloda.fast.api.model.base.BaseVkMessage
|
||||
import com.meloda.fast.api.model.base.BaseVkUser
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class MessagesGetHistoryResponse(
|
||||
val count: Int,
|
||||
val items: List<BaseVkMessage> = listOf(),
|
||||
val conversations: List<BaseVkConversation>?,
|
||||
val profiles: List<BaseVkUser>?,
|
||||
val groups: List<BaseVkGroup>?
|
||||
) : Parcelable
|
||||
@@ -24,18 +24,24 @@ abstract class BaseAdapter<Item, VH : BaseHolder>(
|
||||
|
||||
protected var inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
|
||||
var itemClickListener: OnItemClickListener? = null
|
||||
var itemLongClickListener: OnItemLongClickListener? = null
|
||||
var itemClickListener: ((position: Int) -> Unit) = {}
|
||||
var itemLongClickListener: ((position: Int) -> Boolean) = { false }
|
||||
|
||||
open fun destroy() {
|
||||
itemClickListener = null
|
||||
itemLongClickListener = null
|
||||
}
|
||||
open fun destroy() {}
|
||||
|
||||
override fun getItem(position: Int): Item {
|
||||
return values[position]
|
||||
}
|
||||
|
||||
fun getOrNull(position: Int): Item? {
|
||||
return if (position >= 0 && position <= values.lastIndex) get(position) else null
|
||||
}
|
||||
|
||||
fun getOrElse(position: Int, defaultValue: (Int) -> Item): Item {
|
||||
return if (position >= 0 && position <= values.lastIndex) get(position)
|
||||
else defaultValue(position)
|
||||
}
|
||||
|
||||
fun add(position: Int, item: Item) {
|
||||
values.add(position, item)
|
||||
cleanValues.add(position, item)
|
||||
@@ -103,26 +109,23 @@ abstract class BaseAdapter<Item, VH : BaseHolder>(
|
||||
onBindItemViewHolder(holder, position)
|
||||
}
|
||||
|
||||
private fun onBindItemViewHolder(holder: VH, position: Int) {
|
||||
initListeners(holder.itemView, position)
|
||||
holder.bind(position)
|
||||
}
|
||||
|
||||
protected fun initListeners(itemView: View, position: Int) {
|
||||
if (itemView is AdapterView<*>) return
|
||||
|
||||
itemView.setOnClickListener {
|
||||
itemClickListener?.onItemClick(position)
|
||||
}
|
||||
|
||||
itemView.setOnLongClickListener {
|
||||
itemLongClickListener?.onItemLongClick(position)
|
||||
return@setOnLongClickListener itemClickListener == null
|
||||
}
|
||||
itemView.setOnClickListener { itemClickListener.invoke(position) }
|
||||
itemView.setOnLongClickListener { itemLongClickListener.invoke(position) }
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return values.size
|
||||
}
|
||||
|
||||
private fun onBindItemViewHolder(holder: VH, position: Int) {
|
||||
initListeners(holder.itemView, position)
|
||||
holder.bind(position)
|
||||
}
|
||||
val lastPosition
|
||||
get() = itemCount - 1
|
||||
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import com.meloda.fast.database.dao.UsersDao
|
||||
VkUser::class,
|
||||
VkGroup::class
|
||||
],
|
||||
version = 11,
|
||||
version = 13,
|
||||
exportSchema = false
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
+6
-2
@@ -1,4 +1,4 @@
|
||||
package com.meloda.fast.api
|
||||
package com.meloda.fast.di
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
@@ -28,7 +28,7 @@ import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
class VKModules {
|
||||
object NetworkModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@@ -77,6 +77,10 @@ class VKModules {
|
||||
fun provideUsersRepo(retrofit: Retrofit): UsersRepo =
|
||||
retrofit.create(UsersRepo::class.java)
|
||||
|
||||
@Provides
|
||||
fun provideMessagesRepo(retrofit: Retrofit): MessagesRepo =
|
||||
retrofit.create(MessagesRepo::class.java)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAuthDataSource(
|
||||
+8
-8
@@ -1,4 +1,4 @@
|
||||
package com.meloda.fast.screens.messages
|
||||
package com.meloda.fast.screens.conversations
|
||||
|
||||
import android.content.Context
|
||||
import android.text.SpannableString
|
||||
@@ -17,7 +17,7 @@ import com.meloda.fast.api.model.VkUser
|
||||
import com.meloda.fast.base.adapter.BaseAdapter
|
||||
import com.meloda.fast.base.adapter.BindingHolder
|
||||
import com.meloda.fast.databinding.ItemConversationBinding
|
||||
import java.text.SimpleDateFormat
|
||||
import com.meloda.fast.util.TimeUtils
|
||||
|
||||
class ConversationsAdapter constructor(
|
||||
context: Context,
|
||||
@@ -76,9 +76,9 @@ class ConversationsAdapter constructor(
|
||||
} else null
|
||||
|
||||
val avatar = when {
|
||||
chatUser != null && !chatUser.photo200.isNullOrBlank() -> chatUser.photo200
|
||||
chatGroup != null && !chatGroup.photo200.isNullOrBlank() -> chatGroup.photo200
|
||||
!conversation.photo200.isNullOrBlank() -> conversation.photo200
|
||||
conversation.isUser() && chatUser != null && !chatUser.photo200.isNullOrBlank() -> chatUser.photo200
|
||||
conversation.isGroup() && chatGroup != null && !chatGroup.photo200.isNullOrBlank() -> chatGroup.photo200
|
||||
conversation.isChat() && !conversation.photo200.isNullOrBlank() -> conversation.photo200
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -127,11 +127,11 @@ class ConversationsAdapter constructor(
|
||||
message = message
|
||||
) else null
|
||||
|
||||
val messageText = if (actionMessage != null ||
|
||||
val messageText = (if (actionMessage != null ||
|
||||
forwardsMessage != null ||
|
||||
attachmentText != null
|
||||
) ""
|
||||
else message.text ?: "[no_message]"
|
||||
else message.text ?: "[no_message]").run { VkUtils.prepareMessageText(this) }
|
||||
|
||||
val coloredMessage = actionMessage ?: attachmentText ?: forwardsMessage ?: ""
|
||||
|
||||
@@ -165,7 +165,7 @@ class ConversationsAdapter constructor(
|
||||
binding.title.text =
|
||||
getItem(position).title ?: chatUser?.toString() ?: chatGroup?.name ?: "..."
|
||||
|
||||
binding.date.text = SimpleDateFormat("HH:mm").format(message.date * 1000)
|
||||
binding.date.text = TimeUtils.getLocalizedTime(context, message.date * 1000L)
|
||||
|
||||
binding.container.background = if (conversation.isUnread()) ContextCompat.getDrawable(
|
||||
context,
|
||||
+28
-9
@@ -1,10 +1,12 @@
|
||||
package com.meloda.fast.screens.messages
|
||||
package com.meloda.fast.screens.conversations
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.viewbinding.library.fragment.viewBinding
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import coil.load
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.meloda.fast.R
|
||||
@@ -17,7 +19,6 @@ import com.meloda.fast.base.viewmodel.VKEvent
|
||||
import com.meloda.fast.databinding.FragmentConversationsBinding
|
||||
import com.meloda.fast.util.AndroidUtils
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ConversationsFragment :
|
||||
@@ -39,7 +40,10 @@ class ConversationsFragment :
|
||||
|
||||
prepareViews()
|
||||
|
||||
adapter = ConversationsAdapter(requireContext(), mutableListOf())
|
||||
adapter = ConversationsAdapter(requireContext(), mutableListOf()).also {
|
||||
it.itemClickListener = this::onItemClick
|
||||
it.itemLongClickListener = this::onItemLongClick
|
||||
}
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
viewModel.loadConversations()
|
||||
@@ -86,9 +90,7 @@ class ConversationsFragment :
|
||||
private fun prepareRefreshLayout() {
|
||||
with(binding.refreshLayout) {
|
||||
setProgressViewOffset(
|
||||
true,
|
||||
AndroidUtils.px(40).roundToInt(),
|
||||
AndroidUtils.px(96).roundToInt()
|
||||
true, progressViewStartOffset, progressViewEndOffset
|
||||
)
|
||||
setProgressBackgroundColorSchemeColor(
|
||||
AndroidUtils.getThemeAttrColor(
|
||||
@@ -107,10 +109,7 @@ class ConversationsFragment :
|
||||
}
|
||||
|
||||
private fun refreshConversations(event: ConversationsLoaded) {
|
||||
// adapter.profiles.clear()
|
||||
adapter.profiles += event.profiles
|
||||
|
||||
// adapter.groups.clear()
|
||||
adapter.groups += event.groups
|
||||
|
||||
fillRecyclerView(event.conversations)
|
||||
@@ -122,4 +121,24 @@ class ConversationsFragment :
|
||||
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||
}
|
||||
|
||||
private fun onItemClick(position: Int) {
|
||||
val conversation = adapter[position]
|
||||
val user = if (conversation.isUser()) adapter.profiles[conversation.id] else null
|
||||
val group = if (conversation.isGroup()) adapter.groups[conversation.id] else null
|
||||
|
||||
findNavController().navigate(
|
||||
R.id.toMessagesHistory,
|
||||
bundleOf(
|
||||
"conversation" to adapter[position],
|
||||
"user" to user,
|
||||
"group" to group
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun onItemLongClick(position: Int): Boolean {
|
||||
binding.createChat.performClick()
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
+5
-12
@@ -1,4 +1,4 @@
|
||||
package com.meloda.fast.screens.messages
|
||||
package com.meloda.fast.screens.conversations
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.meloda.fast.api.UserConfig
|
||||
@@ -31,7 +31,7 @@ class ConversationsViewModel @Inject constructor(
|
||||
dataSource.getAllChats(
|
||||
ConversationsGetRequest(
|
||||
count = 30,
|
||||
// offset = 37,
|
||||
// offset = 177,
|
||||
extended = true,
|
||||
fields = "${VKConstants.USER_FIELDS},${VKConstants.GROUP_FIELDS}"
|
||||
)
|
||||
@@ -49,9 +49,6 @@ class ConversationsViewModel @Inject constructor(
|
||||
baseGroup.asVkGroup().let { group -> groups[group.id] = group }
|
||||
}
|
||||
|
||||
// val profiles = response.profiles?.map { profile -> profile.asVkUser() } ?: listOf()
|
||||
// val groups = response.groups?.map { group -> group.asVkGroup() } ?: listOf()
|
||||
|
||||
sendEvent(
|
||||
ConversationsLoaded(
|
||||
count = response.count,
|
||||
@@ -71,12 +68,8 @@ class ConversationsViewModel @Inject constructor(
|
||||
val er = it
|
||||
throw it
|
||||
},
|
||||
onStart = {
|
||||
sendEvent(StartProgressEvent)
|
||||
},
|
||||
onEnd = {
|
||||
sendEvent(StopProgressEvent)
|
||||
})
|
||||
onStart = { sendEvent(StartProgressEvent) },
|
||||
onEnd = { sendEvent(StopProgressEvent) })
|
||||
}
|
||||
|
||||
fun loadProfileUser() = viewModelScope.launch {
|
||||
@@ -96,7 +89,7 @@ class ConversationsViewModel @Inject constructor(
|
||||
|
||||
data class ConversationsLoaded(
|
||||
val count: Int,
|
||||
val unreadCount: Int,
|
||||
val unreadCount: Int?,
|
||||
val conversations: List<VkConversation>,
|
||||
val profiles: HashMap<Int, VkUser>,
|
||||
val groups: HashMap<Int, VkGroup>
|
||||
@@ -0,0 +1,197 @@
|
||||
package com.meloda.fast.screens.messages
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import coil.load
|
||||
import com.meloda.fast.api.model.VkConversation
|
||||
import com.meloda.fast.api.model.VkGroup
|
||||
import com.meloda.fast.api.model.VkMessage
|
||||
import com.meloda.fast.api.model.VkUser
|
||||
import com.meloda.fast.base.adapter.BaseAdapter
|
||||
import com.meloda.fast.base.adapter.BaseHolder
|
||||
import com.meloda.fast.common.AppGlobal
|
||||
import com.meloda.fast.databinding.ItemMessageInBinding
|
||||
import com.meloda.fast.databinding.ItemMessageOutBinding
|
||||
import com.meloda.fast.databinding.ItemMessageServiceBinding
|
||||
import com.meloda.fast.util.AndroidUtils
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class MessagesHistoryAdapter constructor(
|
||||
context: Context,
|
||||
values: MutableList<VkMessage>,
|
||||
val conversation: VkConversation,
|
||||
val profiles: HashMap<Int, VkUser> = hashMapOf(),
|
||||
val groups: HashMap<Int, VkGroup> = hashMapOf()
|
||||
) : 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
|
||||
}
|
||||
|
||||
if (viewType == -1) {
|
||||
getItem(position).let {
|
||||
if (it.action != null) viewType = SERVICE
|
||||
if (it.isOut) viewType = OUTGOING
|
||||
if (!it.isOut) viewType = INCOMING
|
||||
}
|
||||
}
|
||||
|
||||
return viewType
|
||||
}
|
||||
|
||||
private fun isPositionHeader(position: Int) = position == 0
|
||||
private fun isPositionFooter(position: Int) = position >= actualSize
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
|
||||
return when (viewType) {
|
||||
HEADER -> Header(createEmptyView(60))
|
||||
FOOTER -> Footer(createEmptyView(36))
|
||||
SERVICE -> ServiceMessage(ItemMessageServiceBinding.inflate(inflater, parent, false))
|
||||
OUTGOING -> OutgoingMessage(ItemMessageOutBinding.inflate(inflater, parent, false))
|
||||
INCOMING -> IncomingMessage(ItemMessageInBinding.inflate(inflater, parent, false))
|
||||
else -> Holder()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createEmptyView(size: Int) = View(context).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
AndroidUtils.px(size).roundToInt()
|
||||
)
|
||||
|
||||
isEnabled = false
|
||||
isClickable = false
|
||||
isFocusable = false
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||
if (holder is Header || holder is Footer) return
|
||||
|
||||
initListeners(holder.itemView, position)
|
||||
holder.bind(position)
|
||||
}
|
||||
|
||||
open inner class Holder(v: View = View(context)) : BaseHolder(v)
|
||||
|
||||
inner class Header(v: View) : Holder(v)
|
||||
|
||||
inner class Footer(v: View) : Holder(v)
|
||||
|
||||
inner class ServiceMessage(
|
||||
private val binding: ItemMessageServiceBinding
|
||||
) : Holder(binding.root) {
|
||||
|
||||
override fun bind(position: Int) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
inner class OutgoingMessage(
|
||||
private val binding: ItemMessageOutBinding
|
||||
) : 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
inner class IncomingMessage(
|
||||
private val binding: ItemMessageInBinding
|
||||
) : Holder(binding.root) {
|
||||
|
||||
init {
|
||||
binding.bubble.maxWidth = (AppGlobal.screenWidth * 0.7).roundToInt()
|
||||
}
|
||||
|
||||
override fun bind(position: Int) {
|
||||
val message = getItem(position)
|
||||
|
||||
val prevMessage = getOrNull(position - 1)
|
||||
val nextMessage = getOrNull(position + 1)
|
||||
|
||||
binding.title.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()) {
|
||||
profiles[message.fromId]
|
||||
} else null
|
||||
|
||||
val messageGroup: VkGroup? = if (message.isGroup()) {
|
||||
groups[message.fromId]
|
||||
} else null
|
||||
|
||||
val avatar = when {
|
||||
message.isUser() && messageUser != null && !messageUser.photo200.isNullOrBlank() -> messageUser.photo200
|
||||
message.isGroup() && messageGroup != null && !messageGroup.photo200.isNullOrBlank() -> messageGroup.photo200
|
||||
else -> null
|
||||
}
|
||||
|
||||
val title = when {
|
||||
message.isUser() && messageUser != null -> messageUser.firstName
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val actualSize get() = values.size
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
if (actualSize == 0) return 2
|
||||
return super.getItemCount() + 2
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val INCOMING = 1001
|
||||
private const val OUTGOING = 1002
|
||||
private const val SERVICE = 1003
|
||||
private const val HEADER = 0
|
||||
private const val FOOTER = 2
|
||||
|
||||
private val COMPARATOR = object : DiffUtil.ItemCallback<VkMessage>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: VkMessage,
|
||||
newItem: VkMessage
|
||||
) = false
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: VkMessage,
|
||||
newItem: VkMessage
|
||||
) = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package com.meloda.fast.screens.messages
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.viewbinding.library.fragment.viewBinding
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.load
|
||||
import com.meloda.fast.R
|
||||
import com.meloda.fast.api.UserConfig
|
||||
import com.meloda.fast.api.model.VkConversation
|
||||
import com.meloda.fast.api.model.VkGroup
|
||||
import com.meloda.fast.api.model.VkMessage
|
||||
import com.meloda.fast.api.model.VkUser
|
||||
import com.meloda.fast.base.BaseViewModelFragment
|
||||
import com.meloda.fast.base.viewmodel.StartProgressEvent
|
||||
import com.meloda.fast.base.viewmodel.StopProgressEvent
|
||||
import com.meloda.fast.base.viewmodel.VKEvent
|
||||
import com.meloda.fast.databinding.FragmentMessagesHistoryBinding
|
||||
import com.meloda.fast.extensions.TextViewExtensions.clear
|
||||
import com.meloda.fast.util.AndroidUtils
|
||||
import com.meloda.fast.util.TimeUtils
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MessagesHistoryFragment :
|
||||
BaseViewModelFragment<MessagesHistoryViewModel>(R.layout.fragment_messages_history) {
|
||||
|
||||
override val viewModel: MessagesHistoryViewModel by viewModels()
|
||||
private val binding: FragmentMessagesHistoryBinding by viewBinding()
|
||||
|
||||
private val action = MutableLiveData<Action>()
|
||||
|
||||
private enum class Action {
|
||||
RECORD, SEND
|
||||
}
|
||||
|
||||
private val user: VkUser? by lazy {
|
||||
requireArguments().getParcelable("user")
|
||||
}
|
||||
|
||||
private val group: VkGroup? by lazy {
|
||||
requireArguments().getParcelable("group")
|
||||
}
|
||||
|
||||
private val conversation: VkConversation by lazy {
|
||||
requireNotNull(requireArguments().getParcelable("conversation"))
|
||||
}
|
||||
|
||||
private val adapter: MessagesHistoryAdapter by lazy {
|
||||
MessagesHistoryAdapter(requireContext(), mutableListOf(), conversation)
|
||||
}
|
||||
|
||||
private var timestampTimer: Timer? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val title = when {
|
||||
conversation.isChat() -> conversation.title
|
||||
conversation.isUser() -> user?.toString()
|
||||
conversation.isGroup() -> group?.name
|
||||
else -> null
|
||||
}
|
||||
|
||||
binding.title.text = title ?: "..."
|
||||
|
||||
val status = when {
|
||||
conversation.isChat() -> "${conversation.membersCount} members"
|
||||
conversation.isUser() -> if (user?.online == true) "Online" else "Last seen at [...]"
|
||||
conversation.isGroup() -> "[Group status]"
|
||||
else -> null
|
||||
}
|
||||
|
||||
binding.status.text = status ?: "..."
|
||||
|
||||
val avatar = when {
|
||||
conversation.isChat() -> conversation.photo200
|
||||
conversation.isUser() -> user?.photo200
|
||||
conversation.isGroup() -> group?.photo200
|
||||
else -> null
|
||||
}
|
||||
|
||||
binding.avatar.load(avatar) {
|
||||
crossfade(false)
|
||||
error(ColorDrawable(Color.RED))
|
||||
}
|
||||
|
||||
binding.online.isVisible = user?.online == true
|
||||
|
||||
prepareViews()
|
||||
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
viewModel.loadHistory(conversation.id)
|
||||
|
||||
binding.action.setOnClickListener { performAction() }
|
||||
|
||||
binding.recyclerView.addOnLayoutChangeListener { _, i, i2, i3, bottom, i5, i6, i7, oldBottom ->
|
||||
if (bottom >= oldBottom) return@addOnLayoutChangeListener
|
||||
val lastVisiblePosition =
|
||||
(binding.recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
|
||||
|
||||
if (lastVisiblePosition <= adapter.lastPosition - 10) return@addOnLayoutChangeListener
|
||||
|
||||
binding.recyclerView.postDelayed({
|
||||
binding.recyclerView.scrollToPosition(adapter.lastPosition)
|
||||
}, 25)
|
||||
}
|
||||
|
||||
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
val firstPosition =
|
||||
(recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
|
||||
|
||||
val message = adapter.getOrNull(firstPosition)
|
||||
message?.let {
|
||||
binding.timestamp.isVisible = true
|
||||
|
||||
val time = "${
|
||||
TimeUtils.getLocalizedDate(
|
||||
requireContext(),
|
||||
it.date * 1000L
|
||||
)
|
||||
}, ${SimpleDateFormat("HH:mm", Locale.getDefault()).format(it.date * 1000L)}"
|
||||
|
||||
binding.timestamp.text = time
|
||||
|
||||
if (timestampTimer != null) {
|
||||
timestampTimer?.cancel()
|
||||
timestampTimer = null
|
||||
}
|
||||
|
||||
timestampTimer = Timer()
|
||||
timestampTimer?.schedule(2500) {
|
||||
recyclerView.post { binding.timestamp.isVisible = false }
|
||||
}
|
||||
}
|
||||
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
}
|
||||
})
|
||||
|
||||
binding.message.doAfterTextChanged {
|
||||
val newValue = if (it.toString().isNotBlank()) Action.SEND
|
||||
else Action.RECORD
|
||||
|
||||
if (action.value != newValue) action.value = newValue
|
||||
}
|
||||
|
||||
action.observe(viewLifecycleOwner) {
|
||||
|
||||
binding.action.animate()
|
||||
.scaleX(1.25f)
|
||||
.scaleY(1.25f)
|
||||
.setDuration(100)
|
||||
.withEndAction {
|
||||
binding.action.animate()
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.setDuration(100)
|
||||
.start()
|
||||
}.start()
|
||||
|
||||
when (it) {
|
||||
Action.RECORD -> {
|
||||
binding.action.setImageResource(R.drawable.ic_round_mic_24)
|
||||
}
|
||||
Action.SEND -> {
|
||||
binding.action.setImageResource(R.drawable.ic_round_send_24)
|
||||
}
|
||||
else -> return@observe
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun performAction() {
|
||||
if (action.value == Action.RECORD) {
|
||||
|
||||
} else if (action.value == Action.SEND) {
|
||||
val messageText = binding.message.text.toString().trim()
|
||||
if (messageText.isBlank()) return
|
||||
|
||||
val date = System.currentTimeMillis()
|
||||
|
||||
var message = VkMessage(
|
||||
id = -1,
|
||||
text = messageText,
|
||||
isOut = true,
|
||||
peerId = conversation.id,
|
||||
fromId = UserConfig.userId,
|
||||
date = (date / 1000).toInt(),
|
||||
randomId = 0
|
||||
)
|
||||
|
||||
adapter.add(message)
|
||||
adapter.notifyItemRangeInserted(adapter.lastPosition - 1, 1)
|
||||
binding.recyclerView.smoothScrollToPosition(adapter.lastPosition)
|
||||
binding.message.clear()
|
||||
|
||||
viewModel.sendMessage(
|
||||
peerId = conversation.id,
|
||||
message = messageText,
|
||||
randomId = 0
|
||||
) { message = message.changeId(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEvent(event: VKEvent) {
|
||||
when (event) {
|
||||
is MessagesLoaded -> refreshMessages(event)
|
||||
is StartProgressEvent -> onProgressStarted()
|
||||
is StopProgressEvent -> onProgressStopped()
|
||||
}
|
||||
super.onEvent(event)
|
||||
}
|
||||
|
||||
private fun onProgressStarted() {
|
||||
binding.progressBar.isVisible = adapter.isEmpty()
|
||||
binding.refreshLayout.isRefreshing = adapter.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun onProgressStopped() {
|
||||
binding.progressBar.isVisible = false
|
||||
binding.refreshLayout.isRefreshing = false
|
||||
}
|
||||
|
||||
private fun prepareViews() {
|
||||
prepareRecyclerView()
|
||||
prepareRefreshLayout()
|
||||
}
|
||||
|
||||
private fun prepareRecyclerView() {
|
||||
binding.recyclerView.itemAnimator = null
|
||||
}
|
||||
|
||||
private fun prepareRefreshLayout() {
|
||||
with(binding.refreshLayout) {
|
||||
setProgressViewOffset(
|
||||
true, progressViewStartOffset, progressViewEndOffset
|
||||
)
|
||||
setProgressBackgroundColorSchemeColor(
|
||||
AndroidUtils.getThemeAttrColor(
|
||||
requireContext(),
|
||||
R.attr.colorSurface
|
||||
)
|
||||
)
|
||||
setColorSchemeColors(
|
||||
AndroidUtils.getThemeAttrColor(
|
||||
requireContext(),
|
||||
R.attr.colorAccent
|
||||
)
|
||||
)
|
||||
setOnRefreshListener { viewModel.loadHistory(peerId = conversation.id) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshMessages(event: MessagesLoaded) {
|
||||
adapter.profiles += event.profiles
|
||||
adapter.groups += event.groups
|
||||
|
||||
fillRecyclerView(event.messages)
|
||||
}
|
||||
|
||||
private fun fillRecyclerView(values: List<VkMessage>) {
|
||||
val smoothScroll = adapter.isNotEmpty()
|
||||
|
||||
adapter.values.clear()
|
||||
adapter.values += values.sortedBy { it.date }
|
||||
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||
|
||||
if (smoothScroll) binding.recyclerView.smoothScrollToPosition(adapter.lastPosition)
|
||||
else binding.recyclerView.scrollToPosition(adapter.lastPosition)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.meloda.fast.screens.messages
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.meloda.fast.api.datasource.MessagesDataSource
|
||||
import com.meloda.fast.api.model.VkConversation
|
||||
import com.meloda.fast.api.model.VkGroup
|
||||
import com.meloda.fast.api.model.VkMessage
|
||||
import com.meloda.fast.api.model.VkUser
|
||||
import com.meloda.fast.api.network.request.MessagesGetHistoryRequest
|
||||
import com.meloda.fast.api.network.request.MessagesSendRequest
|
||||
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MessagesHistoryViewModel @Inject constructor(
|
||||
private val dataSource: MessagesDataSource
|
||||
) : BaseViewModel() {
|
||||
|
||||
fun loadHistory(
|
||||
peerId: Int
|
||||
) = viewModelScope.launch {
|
||||
makeJob({
|
||||
dataSource.getHistory(
|
||||
MessagesGetHistoryRequest(
|
||||
count = 90,
|
||||
peerId = peerId,
|
||||
extended = true,
|
||||
fields = "photo_200,sex"
|
||||
)
|
||||
)
|
||||
},
|
||||
onAnswer = {
|
||||
val response = it.response ?: return@makeJob
|
||||
|
||||
val profiles = hashMapOf<Int, VkUser>()
|
||||
response.profiles?.let { baseProfiles ->
|
||||
baseProfiles.forEach { baseProfile ->
|
||||
baseProfile.asVkUser().let { profile -> profiles[profile.id] = profile }
|
||||
}
|
||||
}
|
||||
|
||||
val groups = hashMapOf<Int, VkGroup>()
|
||||
response.groups?.let { baseGroups ->
|
||||
baseGroups.forEach { baseGroup ->
|
||||
baseGroup.asVkGroup().let { group -> groups[group.id] = group }
|
||||
}
|
||||
}
|
||||
|
||||
val messages = hashMapOf<Int, VkMessage>()
|
||||
response.items.forEach { baseMessage ->
|
||||
baseMessage.asVkMessage().let { message -> messages[message.id] = message }
|
||||
}
|
||||
|
||||
val conversations = hashMapOf<Int, VkConversation>()
|
||||
response.conversations?.let { baseConversations ->
|
||||
baseConversations.forEach { baseConversation ->
|
||||
baseConversation.asVkConversation(
|
||||
messages[baseConversation.lastMessageId]
|
||||
).let { conversation -> conversations[conversation.id] = conversation }
|
||||
}
|
||||
}
|
||||
|
||||
sendEvent(
|
||||
MessagesLoaded(
|
||||
count = response.count,
|
||||
profiles = profiles,
|
||||
groups = groups,
|
||||
conversations = conversations,
|
||||
messages = messages.values.toList()
|
||||
)
|
||||
)
|
||||
},
|
||||
onError = {
|
||||
val throwable = it
|
||||
throw it
|
||||
},
|
||||
onStart = { sendEvent(StartProgressEvent) },
|
||||
onEnd = { sendEvent(StopProgressEvent) })
|
||||
}
|
||||
|
||||
fun sendMessage(
|
||||
peerId: Int,
|
||||
message: String? = null,
|
||||
randomId: Int = 0,
|
||||
setId: ((messageId: Int) -> Unit)? = null
|
||||
) = viewModelScope.launch {
|
||||
makeJob(
|
||||
{
|
||||
dataSource.send(
|
||||
MessagesSendRequest(
|
||||
peerId = peerId,
|
||||
randomId = randomId,
|
||||
message = message
|
||||
)
|
||||
)
|
||||
},
|
||||
onAnswer = {
|
||||
val response = it.response ?: return@makeJob
|
||||
setId?.invoke(response)
|
||||
},
|
||||
onError = {
|
||||
val throwable = it
|
||||
val i = 0
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class MessagesLoaded(
|
||||
val count: Int,
|
||||
val conversations: HashMap<Int, VkConversation>,
|
||||
val messages: List<VkMessage>,
|
||||
val profiles: HashMap<Int, VkUser>,
|
||||
val groups: HashMap<Int, VkGroup>
|
||||
) : VKEvent()
|
||||
@@ -71,8 +71,6 @@ object AndroidUtils {
|
||||
}
|
||||
|
||||
return color
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.meloda.fast.util
|
||||
|
||||
import android.content.Context
|
||||
import com.meloda.fast.R
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
object TimeUtils {
|
||||
@@ -14,4 +17,58 @@ object TimeUtils {
|
||||
}.timeInMillis
|
||||
}
|
||||
|
||||
fun getLocalizedDate(context: Context, date: Long): String {
|
||||
val now = Calendar.getInstance()
|
||||
val then = Calendar.getInstance().also { it.timeInMillis = date }
|
||||
|
||||
val pattern =
|
||||
if (now[Calendar.YEAR] != then[Calendar.YEAR]) {
|
||||
"dd MMM yyyy"
|
||||
} else if (now[Calendar.MONTH] != then[Calendar.MONTH]) {
|
||||
"dd MMMM"
|
||||
} else if (now[Calendar.DAY_OF_MONTH] != then[Calendar.DAY_OF_MONTH]) {
|
||||
if (now[Calendar.DAY_OF_MONTH] - then[Calendar.DAY_OF_MONTH] == 1) {
|
||||
return context.getString(R.string.yesterday)
|
||||
} else {
|
||||
"dd MMMM"
|
||||
}
|
||||
} else {
|
||||
return context.getString(R.string.today)
|
||||
}
|
||||
|
||||
return SimpleDateFormat(pattern, Locale.getDefault()).format(date)
|
||||
}
|
||||
|
||||
fun getLocalizedTime(context: Context, date: Long): String {
|
||||
val now = Calendar.getInstance()
|
||||
val then = Calendar.getInstance().also { it.timeInMillis = date }
|
||||
|
||||
return when {
|
||||
now[Calendar.YEAR] != then[Calendar.YEAR] -> {
|
||||
"${now[Calendar.YEAR] - then[Calendar.YEAR]}${
|
||||
context.getString(R.string.year_short).lowercase()
|
||||
}"
|
||||
}
|
||||
now[Calendar.MONTH] != then[Calendar.MONTH] -> {
|
||||
"${now[Calendar.MONTH] - then[Calendar.MONTH]}${
|
||||
context.getString(R.string.month_short).lowercase()
|
||||
}"
|
||||
}
|
||||
now[Calendar.DAY_OF_MONTH] != then[Calendar.DAY_OF_MONTH] -> {
|
||||
val change = now[Calendar.DAY_OF_MONTH] - then[Calendar.DAY_OF_MONTH]
|
||||
if (change >= 7) {
|
||||
"${change / 7}${context.getString(R.string.week_short).lowercase()}"
|
||||
} else {
|
||||
"$change${context.getString(R.string.day_short).lowercase()}"
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (now[Calendar.MINUTE] == then[Calendar.MINUTE]) {
|
||||
context.getString(R.string.time_now).lowercase()
|
||||
} else {
|
||||
SimpleDateFormat("HH:mm", Locale.getDefault()).format(date)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.meloda.fast.widget
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
import com.meloda.fast.R
|
||||
|
||||
class BoundedFrameLayout : FrameLayout {
|
||||
private var mBoundedWidth: Int
|
||||
private var mBoundedHeight: Int
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
mBoundedWidth = 0
|
||||
mBoundedHeight = 0
|
||||
}
|
||||
|
||||
@SuppressLint("CustomViewStyleable")
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
|
||||
val a = context.obtainStyledAttributes(attrs, R.styleable.BoundedView)
|
||||
mBoundedWidth = a.getDimensionPixelSize(R.styleable.BoundedView_bounded_width, 0)
|
||||
mBoundedHeight = a.getDimensionPixelSize(R.styleable.BoundedView_bounded_height, 0)
|
||||
a.recycle()
|
||||
}
|
||||
|
||||
var maxWidth: Int
|
||||
get() = mBoundedWidth
|
||||
set(width) {
|
||||
if (mBoundedWidth != width) {
|
||||
mBoundedWidth = width
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
var maxHeight: Int
|
||||
get() = mBoundedHeight
|
||||
set(height) {
|
||||
if (mBoundedHeight != height) {
|
||||
mBoundedHeight = height
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
// Adjust width as necessary
|
||||
var widthMeasureSpec = widthMeasureSpec
|
||||
var heightMeasureSpec = heightMeasureSpec
|
||||
val measuredWidth = MeasureSpec.getSize(widthMeasureSpec)
|
||||
|
||||
if (mBoundedWidth in 1 until measuredWidth) {
|
||||
val measureMode = MeasureSpec.getMode(widthMeasureSpec)
|
||||
widthMeasureSpec = MeasureSpec.makeMeasureSpec(mBoundedWidth, measureMode)
|
||||
}
|
||||
|
||||
// Adjust height as necessary
|
||||
val measuredHeight = MeasureSpec.getSize(heightMeasureSpec)
|
||||
if (mBoundedHeight in 1 until measuredHeight) {
|
||||
val measureMode = MeasureSpec.getMode(heightMeasureSpec)
|
||||
heightMeasureSpec = MeasureSpec.makeMeasureSpec(mBoundedHeight, measureMode)
|
||||
}
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,7 +10,7 @@ class BoundedLinearLayout : LinearLayout {
|
||||
private var mBoundedWidth: Int
|
||||
private var mBoundedHeight: Int
|
||||
|
||||
constructor(context: Context?) : super(context) {
|
||||
constructor(context: Context) : super(context) {
|
||||
mBoundedWidth = 0
|
||||
mBoundedHeight = 0
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.meloda.fast.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import com.google.android.material.textview.MaterialTextView
|
||||
|
||||
class ScrollingTextView : MaterialTextView {
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
)
|
||||
|
||||
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
|
||||
if (focused) super.onFocusChanged(focused, direction, previouslyFocusedRect)
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(focused: Boolean) {
|
||||
if (focused) super.onWindowFocusChanged(true)
|
||||
}
|
||||
|
||||
override fun isFocused(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -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="30dp"
|
||||
android:topRightRadius="40dp" />
|
||||
|
||||
</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="@android:color/white" />
|
||||
|
||||
<corners
|
||||
android:bottomLeftRadius="40dp"
|
||||
android:bottomRightRadius="5dp"
|
||||
android:topLeftRadius="40dp"
|
||||
android:topRightRadius="30dp" />
|
||||
|
||||
</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">
|
||||
|
||||
<corners
|
||||
android:bottomLeftRadius="30dp"
|
||||
android:bottomRightRadius="5dp"
|
||||
android:topLeftRadius="30dp"
|
||||
android:topRightRadius="30dp" />
|
||||
|
||||
<solid android:color="@android:color/white" />
|
||||
|
||||
</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">
|
||||
|
||||
<gradient
|
||||
android:angle="90"
|
||||
android:centerColor="#ffffff"
|
||||
android:centerY="0.625"
|
||||
android:endColor="@android:color/transparent"
|
||||
android:startColor="#ffffff"
|
||||
android:type="linear" />
|
||||
|
||||
</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">
|
||||
|
||||
<gradient
|
||||
android:angle="-90"
|
||||
android:centerColor="#ffffff"
|
||||
android:centerY="0.625"
|
||||
android:endColor="@android:color/transparent"
|
||||
android:startColor="#ffffff"
|
||||
android:type="linear" />
|
||||
|
||||
</shape>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,14c1.66,0 3,-1.34 3,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.91,11c-0.49,0 -0.9,0.36 -0.98,0.85C16.52,14.2 14.47,16 12,16s-4.52,-1.8 -4.93,-4.15c-0.08,-0.49 -0.49,-0.85 -0.98,-0.85 -0.61,0 -1.09,0.54 -1,1.14 0.49,3 2.89,5.35 5.91,5.78L11,20c0,0.55 0.45,1 1,1s1,-0.45 1,-1v-2.08c3.02,-0.43 5.42,-2.78 5.91,-5.78 0.1,-0.6 -0.39,-1.14 -1,-1.14z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3.4,20.4l17.45,-7.48c0.81,-0.35 0.81,-1.49 0,-1.84L3.4,3.6c-0.66,-0.29 -1.39,0.2 -1.39,0.91L2,9.12c0,0.5 0.37,0.93 0.87,0.99L17,12 2.87,13.88c-0.5,0.07 -0.87,0.5 -0.87,1l0.01,4.61c0,0.71 0.73,1.2 1.39,0.91z"/>
|
||||
</vector>
|
||||
@@ -1,7 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
@@ -103,4 +105,5 @@
|
||||
app:tint="@color/a2_700"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</layout>
|
||||
@@ -0,0 +1,247 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/refreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="86dp">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:itemCount="100"
|
||||
tools:listitem="@layout/item_message_out" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/toolbarContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="true"
|
||||
android:background="@drawable/ic_messages_history_toolbar_gradient_background"
|
||||
android:backgroundTint="@color/n1_50"
|
||||
android:minHeight="140dp">
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="30dp"
|
||||
android:paddingTop="18dp"
|
||||
android:paddingBottom="24dp">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp">
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:src="@tools:sample/avatars"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/avatarPlaceholder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:src="@color/n1_50" />
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:src="@drawable/ic_account_circle_cut"
|
||||
app:tint="@color/n2_500" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/online"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@color/n1_50" />
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_online_pc"
|
||||
app:tint="@color/a3_200" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/service"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="end|top"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/ic_back"
|
||||
android:backgroundTint="@color/n2_500"
|
||||
android:elevation="0.5dp" />
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/phantomIcon"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_gravity="center"
|
||||
android:elevation="1dp"
|
||||
android:src="@drawable/ic_phantom"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/n2_10"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/callIcon"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_gravity="center"
|
||||
android:elevation="1dp"
|
||||
android:src="@drawable/ic_attachment_group_call"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/n2_0" />
|
||||
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:textColor="@color/n1_900"
|
||||
android:textSize="24sp"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="0.7"
|
||||
android:textColor="@color/n1_900"
|
||||
tools:text="Online" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/timestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal|bottom"
|
||||
android:layout_marginBottom="30dp"
|
||||
android:elevation="2dp"
|
||||
android:enabled="false"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="4dp"
|
||||
android:textColor="@color/n1_900"
|
||||
android:visibility="gone"
|
||||
app:chipBackgroundColor="@color/n1_100"
|
||||
app:textEndPadding="12dp"
|
||||
app:textStartPadding="12dp"
|
||||
tools:text="today"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@drawable/ic_message_panel_gradient"
|
||||
android:backgroundTint="@color/n1_50" />
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/messagePanel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_margin="12dp"
|
||||
android:background="@drawable/ic_message_panel_background"
|
||||
android:backgroundTint="@color/a1_0"
|
||||
android:clickable="true"
|
||||
android:elevation="3dp"
|
||||
android:focusable="true"
|
||||
android:minHeight="60dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:hint="@string/message_input_hint" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/attach"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="18dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_baseline_attach_file_24"
|
||||
android:tint="@color/a1_500" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="18dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_round_mic_24"
|
||||
android:tint="@color/a1_500" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
</layout>
|
||||
@@ -6,9 +6,8 @@
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:orientation="horizontal"
|
||||
android:paddingVertical="4dp">
|
||||
android:layout_marginVertical="4dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/container"
|
||||
@@ -30,13 +29,13 @@
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:src="@tools:sample/avatars"
|
||||
tools:visibility="gone" />
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/avatarPlaceholder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:layout_width="match_parent"
|
||||
@@ -51,6 +50,28 @@
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/online"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@color/n1_50" />
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_online_pc"
|
||||
app:tint="@color/a3_200" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/service"
|
||||
@@ -90,31 +111,6 @@
|
||||
app:tint="@color/n2_0" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/online"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@color/n1_50" />
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_online_pc"
|
||||
app:tint="@color/a3_200" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
@@ -221,6 +217,11 @@
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?selectableItemBackground" />
|
||||
</FrameLayout>
|
||||
|
||||
</layout>
|
||||
@@ -0,0 +1,59 @@
|
||||
<?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">
|
||||
|
||||
<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" />
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
</layout>
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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|bottom"
|
||||
android:paddingHorizontal="12dp"
|
||||
android:paddingVertical="2.5dp">
|
||||
|
||||
<com.meloda.fast.widget.CircleImageView
|
||||
android:id="@+id/unread"
|
||||
android:layout_width="13dp"
|
||||
android:layout_height="13dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:src="@color/a3_200" />
|
||||
|
||||
<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:padding="1.5dp"
|
||||
tools:ignore="UselessParent">
|
||||
|
||||
<com.meloda.fast.widget.BoundedFrameLayout
|
||||
android:id="@+id/bubble"
|
||||
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">
|
||||
|
||||
<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|start"
|
||||
android:padding="15dp"
|
||||
android:textColor="@color/n1_800"
|
||||
tools:text="This is test" />
|
||||
|
||||
</com.meloda.fast.widget.BoundedFrameLayout>
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
</layout>
|
||||
@@ -0,0 +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">
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Service" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
</layout>
|
||||
@@ -7,8 +7,20 @@
|
||||
|
||||
<fragment
|
||||
android:id="@+id/conversationsFragment"
|
||||
android:name="com.meloda.fast.screens.messages.ConversationsFragment"
|
||||
android:name="com.meloda.fast.screens.conversations.ConversationsFragment"
|
||||
android:label="ConversationsFragment"
|
||||
tools:layout="@layout/fragment_conversations" />
|
||||
tools:layout="@layout/fragment_conversations">
|
||||
|
||||
<action
|
||||
android:id="@+id/toMessagesHistory"
|
||||
app:destination="@+id/messagesHistoryFragment" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/messagesHistoryFragment"
|
||||
android:name="com.meloda.fast.screens.messages.MessagesHistoryFragment"
|
||||
android:label="MessagesHistoryFragment"
|
||||
tools:layout="@layout/fragment_messages_history" />
|
||||
|
||||
</navigation>
|
||||
@@ -9,13 +9,17 @@
|
||||
<color name="a2_700">@android:color/system_accent2_700</color>
|
||||
|
||||
<color name="a3_200">@android:color/system_accent3_200</color>
|
||||
<color name="a3_700">@android:color/system_accent3_700</color>
|
||||
|
||||
<color name="n1_10">@android:color/system_neutral1_10</color>
|
||||
<color name="n1_50">@android:color/system_neutral1_50</color>
|
||||
<color name="n1_100">@android:color/system_neutral1_100</color>
|
||||
<color name="n1_800">@android:color/system_neutral1_800</color>
|
||||
<color name="n1_900">@android:color/system_neutral1_900</color>
|
||||
|
||||
<color name="n2_0">@android:color/system_neutral2_0</color>
|
||||
<color name="n2_10">@android:color/system_neutral2_10</color>
|
||||
<color name="n2_100">@android:color/system_neutral2_100</color>
|
||||
<color name="n2_500">@android:color/system_neutral2_500</color>
|
||||
|
||||
</resources>
|
||||
@@ -9,15 +9,17 @@
|
||||
<color name="a2_700">#414757</color>
|
||||
|
||||
<color name="a3_200">#DEBAE5</color>
|
||||
<color name="a3_700">#583C61</color>
|
||||
|
||||
<color name="n1_10">#FBF9FC</color>
|
||||
<color name="n1_50">#F1F1F1</color>
|
||||
<color name="n1_100">#E2E1E5</color>
|
||||
<color name="n1_800">#303033</color>
|
||||
<color name="n1_900">#1B1B1D</color>
|
||||
|
||||
<color name="n2_0">#FFFFFF</color>
|
||||
<color name="n2_10">#FDFBFE</color>
|
||||
<color name="n2_100">#E0E2EB</color>
|
||||
<color name="n2_500">#74767D</color>
|
||||
|
||||
<!-- Dark Theme Colors -->
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -34,4 +34,15 @@
|
||||
|
||||
<string name="messages_self_destructed">Messages self-destructed</string>
|
||||
|
||||
<string name="yesterday">Yesterday</string>
|
||||
|
||||
<string name="today">Today</string>
|
||||
|
||||
<string name="year_short">Y</string>
|
||||
<string name="month_short">M</string>
|
||||
<string name="week_short">W</string>
|
||||
<string name="day_short">D</string>
|
||||
<string name="time_now">Now</string>
|
||||
<string name="message_input_hint">Start typing here...</string>
|
||||
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user