messages pin & unpin feature

fix avatars and titles
visual improvements
other bugfixes & minor changes
This commit is contained in:
2021-10-08 12:55:22 +03:00
parent 9e074dd5ad
commit 7c1a7d8a89
32 changed files with 504 additions and 313 deletions
@@ -10,27 +10,28 @@ import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class VkConversation( data class VkConversation(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
val id: Int, var id: Int,
val ownerId: Int?, var ownerId: Int?,
val title: String?, var title: String?,
val photo200: String?, var photo200: String?,
val type: String, var type: String,
val callInProgress: Boolean, var callInProgress: Boolean,
val isPhantom: Boolean, var isPhantom: Boolean,
val lastConversationMessageId: Int, var lastConversationMessageId: Int,
val inRead: Int, var inRead: Int,
val outRead: Int, var outRead: Int,
val isMarkedUnread: Boolean, var isMarkedUnread: Boolean,
val lastMessageId: Int, var lastMessageId: Int,
val unreadCount: Int?, var unreadCount: Int?,
val membersCount: Int?, var membersCount: Int?,
val isPinned: Boolean, var isPinned: Boolean,
var canChangePin: Boolean,
@Embedded(prefix = "pinnedMessage_") @Embedded(prefix = "pinnedMessage_")
var pinnedMessage: VkMessage? = null, var pinnedMessage: VkMessage? = null,
@Embedded(prefix = "lastMessage_") @Embedded(prefix = "lastMessage_")
var lastMessage: VkMessage? = null var lastMessage: VkMessage? = null,
) : Parcelable { ) : Parcelable {
fun isChat() = type == "chat" fun isChat() = type == "chat"
@@ -4,8 +4,10 @@ import androidx.lifecycle.MutableLiveData
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Ignore import androidx.room.Ignore
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.model.attachments.VkAttachment import com.meloda.fast.api.model.attachments.VkAttachment
import com.meloda.fast.base.adapter.SelectableItem import com.meloda.fast.base.adapter.SelectableItem
import com.meloda.fast.util.TimeUtils
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@@ -58,6 +60,10 @@ data class VkMessage(
return Action.parse(action) return Action.parse(action)
} }
fun canEdit() =
fromId == UserConfig.userId &&
(System.currentTimeMillis() / 1000 - date.toLong() < TimeUtils.ONE_DAY_IN_SECONDS)
fun copyMessage( fun copyMessage(
id: Int = this.id, id: Int = this.id,
text: String? = this.text, text: String? = this.text,
@@ -40,7 +40,8 @@ data class BaseVkConversation(
unreadCount = unread_count, unreadCount = unread_count,
membersCount = chat_settings?.members_count, membersCount = chat_settings?.members_count,
ownerId = chat_settings?.owner_id, ownerId = chat_settings?.owner_id,
isPinned = sort_id.major_id > 0 isPinned = sort_id.major_id > 0,
canChangePin = chat_settings?.acl?.can_change_pin == true
).apply { ).apply {
this.lastMessage = lastMessage this.lastMessage = lastMessage
this.pinnedMessage = chat_settings?.pinned_message?.asVkMessage() this.pinnedMessage = chat_settings?.pinned_message?.asVkMessage()
@@ -70,6 +70,28 @@ data class MessagesMarkAsImportantRequest(
} }
@Parcelize
data class MessagesPinMessageRequest(
val peerId: Int,
val messageId: Int? = null,
val conversationMessageId: Int? = null
) : Parcelable {
val map
get() = mutableMapOf(
"peer_id" to peerId.toString()
).apply {
messageId?.let { this["message_id"] = it.toString() }
conversationMessageId?.let { this["conversation_message_id"] = it.toString() }
}
}
@Parcelize
data class MessagesUnPinMessageRequest(val peerId: Int) : Parcelable {
val map get() = mutableMapOf("peer_id" to peerId.toString())
}
@Parcelize @Parcelize
data class MessagesGetLongPollServerRequest( data class MessagesGetLongPollServerRequest(
val needPts: Boolean, val needPts: Boolean,
@@ -1,6 +1,6 @@
package com.meloda.fast.api.network package com.meloda.fast.api.network
object VKUrls { object VkUrls {
const val OAUTH = "https://oauth.vk.com" const val OAUTH = "https://oauth.vk.com"
const val API = "https://api.vk.com/method" const val API = "https://api.vk.com/method"
@@ -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 Pin = "$API/messages.pin"
const val Unpin = "$API/messages.unpin"
const val GetLongPollServer = "$API/messages.getLongPollServer" const val GetLongPollServer = "$API/messages.getLongPollServer"
const val GetLongPollHistory = "$API/messages.getLongPollHistory" const val GetLongPollHistory = "$API/messages.getLongPollHistory"
} }
@@ -1,10 +1,7 @@
package com.meloda.fast.api.network.datasource package com.meloda.fast.api.network.datasource
import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.request.MessagesGetHistoryRequest import com.meloda.fast.api.model.request.*
import com.meloda.fast.api.model.request.MessagesGetLongPollServerRequest
import com.meloda.fast.api.model.request.MessagesMarkAsImportantRequest
import com.meloda.fast.api.model.request.MessagesSendRequest
import com.meloda.fast.api.network.repo.MessagesRepo import com.meloda.fast.api.network.repo.MessagesRepo
import com.meloda.fast.database.dao.MessagesDao import com.meloda.fast.database.dao.MessagesDao
import javax.inject.Inject import javax.inject.Inject
@@ -26,8 +23,14 @@ class MessagesDataSource @Inject constructor(
suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) = suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) =
repo.getLongPollServer(params.map) repo.getLongPollServer(params.map)
suspend fun storeMessages(messages: List<VkMessage>) = dao.insert(messages) suspend fun pin(params: MessagesPinMessageRequest) =
repo.pin(params.map)
suspend fun getCachedMessages(peerId: Int) = dao.getByPeerId(peerId) suspend fun unpin(params: MessagesUnPinMessageRequest) =
repo.unpin(params.map)
suspend fun store(messages: List<VkMessage>) = dao.insert(messages)
suspend fun getCached(peerId: Int) = dao.getByPeerId(peerId)
} }
@@ -1,6 +1,6 @@
package com.meloda.fast.api.network.repo package com.meloda.fast.api.network.repo
import com.meloda.fast.api.network.VKUrls import com.meloda.fast.api.network.VkUrls
import com.meloda.fast.api.model.response.ResponseAuthDirect import com.meloda.fast.api.model.response.ResponseAuthDirect
import com.meloda.fast.api.network.Answer import com.meloda.fast.api.network.Answer
import com.meloda.fast.api.model.response.ResponseSendSms import com.meloda.fast.api.model.response.ResponseSendSms
@@ -8,10 +8,10 @@ import retrofit2.http.*
interface AuthRepo { interface AuthRepo {
@GET(VKUrls.Auth.DirectAuth) @GET(VkUrls.Auth.DirectAuth)
suspend fun auth(@QueryMap param: Map<String, String?>): Answer<ResponseAuthDirect> 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> suspend fun sendSms(@Query("sid") validationSid: String): Answer<ResponseSendSms>
} }
@@ -2,7 +2,7 @@ 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.network.Answer import com.meloda.fast.api.network.Answer
import com.meloda.fast.api.network.VKUrls import com.meloda.fast.api.network.VkUrls
import com.meloda.fast.api.model.response.ConversationsGetResponse import com.meloda.fast.api.model.response.ConversationsGetResponse
import retrofit2.http.FieldMap import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded import retrofit2.http.FormUrlEncoded
@@ -11,7 +11,7 @@ import retrofit2.http.POST
interface ConversationsRepo { interface ConversationsRepo {
@FormUrlEncoded @FormUrlEncoded
@POST(VKUrls.Conversations.Get) @POST(VkUrls.Conversations.Get)
suspend fun getAllChats(@FieldMap params: Map<String, String>): Answer<ApiResponse<ConversationsGetResponse>> suspend fun getAllChats(@FieldMap params: Map<String, String>): Answer<ApiResponse<ConversationsGetResponse>>
} }
@@ -2,9 +2,10 @@ 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.base.BaseVkLongPoll
import com.meloda.fast.api.model.base.BaseVkMessage
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
import retrofit2.http.FieldMap import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST import retrofit2.http.POST
@@ -12,19 +13,27 @@ import retrofit2.http.POST
interface MessagesRepo { interface MessagesRepo {
@FormUrlEncoded @FormUrlEncoded
@POST(VKUrls.Messages.GetHistory) @POST(VkUrls.Messages.GetHistory)
suspend fun getHistory(@FieldMap params: Map<String, String>): Answer<ApiResponse<MessagesGetHistoryResponse>> suspend fun getHistory(@FieldMap params: Map<String, String>): Answer<ApiResponse<MessagesGetHistoryResponse>>
@FormUrlEncoded @FormUrlEncoded
@POST(VKUrls.Messages.Send) @POST(VkUrls.Messages.Send)
suspend fun send(@FieldMap params: Map<String, String>): Answer<ApiResponse<Int>> suspend fun send(@FieldMap params: Map<String, String>): Answer<ApiResponse<Int>>
@FormUrlEncoded @FormUrlEncoded
@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 @FormUrlEncoded
@POST(VKUrls.Messages.GetLongPollServer) @POST(VkUrls.Messages.GetLongPollServer)
suspend fun getLongPollServer(@FieldMap params: Map<String, String>): Answer<ApiResponse<BaseVkLongPoll>> suspend fun getLongPollServer(@FieldMap params: Map<String, String>): Answer<ApiResponse<BaseVkLongPoll>>
@FormUrlEncoded
@POST(VkUrls.Messages.Pin)
suspend fun pin(@FieldMap params: Map<String, String>): Answer<ApiResponse<BaseVkMessage>>
@FormUrlEncoded
@POST(VkUrls.Messages.Unpin)
suspend fun unpin(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
} }
@@ -3,7 +3,7 @@ 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.BaseVkUser import com.meloda.fast.api.model.base.BaseVkUser
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
import retrofit2.http.FieldMap import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST import retrofit2.http.POST
@@ -11,7 +11,7 @@ import retrofit2.http.POST
interface UsersRepo { interface UsersRepo {
@FormUrlEncoded @FormUrlEncoded
@POST(VKUrls.Users.GetById) @POST(VkUrls.Users.GetById)
suspend fun getById( suspend fun getById(
@FieldMap params: Map<String, String>? @FieldMap params: Map<String, String>?
): Answer<ApiResponse<List<BaseVkUser>>> ): Answer<ApiResponse<List<BaseVkUser>>>
@@ -11,7 +11,7 @@ import com.meloda.fast.activity.MainActivity
import com.meloda.fast.api.UserConfig 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.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
@@ -30,7 +30,7 @@ abstract class BaseViewModelFragment<VM : BaseViewModel> : BaseFragment {
} }
} }
protected open fun onEvent(event: VKEvent) { protected open fun onEvent(event: VkEvent) {
if (event is IllegalTokenEvent) { if (event is IllegalTokenEvent) {
Toast.makeText( Toast.makeText(
requireContext(), R.string.authorization_failed, Toast.LENGTH_LONG requireContext(), R.string.authorization_failed, Toast.LENGTH_LONG
@@ -15,7 +15,7 @@ abstract class BaseViewModel : ViewModel() {
var unknownErrorDefaultText: String = "" var unknownErrorDefaultText: String = ""
protected val tasksEventChannel = Channel<VKEvent>() protected val tasksEventChannel = Channel<VkEvent>()
val tasksEvent = tasksEventChannel.receiveAsFlow() val tasksEvent = tasksEventChannel.receiveAsFlow()
protected fun <T> makeJob( protected fun <T> makeJob(
@@ -25,22 +25,35 @@ abstract class BaseViewModel : ViewModel() {
onEnd: (suspend () -> Unit)? = null, onEnd: (suspend () -> Unit)? = null,
onError: (suspend (Throwable) -> Unit)? = null onError: (suspend (Throwable) -> Unit)? = null
) = viewModelScope.launch { ) = viewModelScope.launch {
onStart?.invoke() onStart?.invoke() ?: onStart()
when (val response = job()) { when (val response = job()) {
is Answer.Success -> onAnswer(response.data) is Answer.Success -> onAnswer(response.data)
is Answer.Error -> { is Answer.Error -> {
checkErrors(response.throwable) checkErrors(response.throwable)
onError?.invoke(response.throwable) ?: sendEvent( onError?.invoke(response.throwable) ?: onError(response.throwable)
ErrorEvent(
response.throwable.message
?: unknownErrorDefaultText
)
)
} }
} }
}.also { it.invokeOnCompletion { viewModelScope.launch { onEnd?.invoke() } } } }.also {
it.invokeOnCompletion {
viewModelScope.launch {
onEnd?.invoke() ?: onStop()
}
}
}
protected suspend fun <T : VKEvent> sendEvent(event: T) = tasksEventChannel.send(event) protected suspend fun onStart() {
sendEvent(StartProgressEvent)
}
protected suspend fun onStop() {
sendEvent(StopProgressEvent)
}
protected suspend fun onError(throwable: Throwable) {
sendEvent(ErrorEvent(throwable.message ?: unknownErrorDefaultText))
}
protected suspend fun <T : VkEvent> sendEvent(event: T) = tasksEventChannel.send(event)
private suspend fun checkErrors(throwable: Throwable) { private suspend fun checkErrors(throwable: Throwable) {
when (throwable) { when (throwable) {
@@ -5,15 +5,15 @@ data class ShowDialogInfoEvent(
val message: String, val message: String,
val positiveBtn: String? = null, val positiveBtn: String? = null,
val negativeBtn: String? = null val negativeBtn: String? = null
) : VKEvent() ) : VkEvent()
data class ErrorEvent(val errorText: String) : VKEvent() data class ErrorEvent(val errorText: String) : VkEvent()
object IllegalTokenEvent : VKEvent() object IllegalTokenEvent : VkEvent()
data class CaptchaEvent(val sid: String, val image: String) : VKEvent() data class CaptchaEvent(val sid: String, val image: String) : VkEvent()
data class ValidationEvent(val sid: String) : VKEvent() data class ValidationEvent(val sid: String) : VkEvent()
object StartProgressEvent : VKEvent() object StartProgressEvent : VkEvent()
object StopProgressEvent : VKEvent() object StopProgressEvent : VkEvent()
abstract class VKEvent abstract class VkEvent
@@ -19,7 +19,7 @@ import com.meloda.fast.database.dao.UsersDao
VkUser::class, VkUser::class,
VkGroup::class VkGroup::class
], ],
version = 25, version = 26,
exportSchema = false, exportSchema = false,
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
@@ -24,7 +24,7 @@ import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.base.BaseViewModelFragment import com.meloda.fast.base.BaseViewModelFragment
import com.meloda.fast.base.viewmodel.StartProgressEvent import com.meloda.fast.base.viewmodel.StartProgressEvent
import com.meloda.fast.base.viewmodel.StopProgressEvent import com.meloda.fast.base.viewmodel.StopProgressEvent
import com.meloda.fast.base.viewmodel.VKEvent import com.meloda.fast.base.viewmodel.VkEvent
import com.meloda.fast.common.AppGlobal import com.meloda.fast.common.AppGlobal
import com.meloda.fast.common.AppSettings import com.meloda.fast.common.AppSettings
import com.meloda.fast.common.dataStore import com.meloda.fast.common.dataStore
@@ -185,7 +185,7 @@ class ConversationsFragment :
.show() .show()
} }
override fun onEvent(event: VKEvent) { override fun onEvent(event: VkEvent) {
super.onEvent(event) super.onEvent(event)
when (event) { when (event) {
is ConversationsLoaded -> refreshConversations(event) is ConversationsLoaded -> refreshConversations(event)
@@ -11,9 +11,7 @@ import com.meloda.fast.api.model.request.UsersGetRequest
import com.meloda.fast.api.network.datasource.ConversationsDataSource import com.meloda.fast.api.network.datasource.ConversationsDataSource
import com.meloda.fast.api.network.datasource.UsersDataSource import com.meloda.fast.api.network.datasource.UsersDataSource
import com.meloda.fast.base.viewmodel.BaseViewModel import com.meloda.fast.base.viewmodel.BaseViewModel
import com.meloda.fast.base.viewmodel.StartProgressEvent import com.meloda.fast.base.viewmodel.VkEvent
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.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -66,13 +64,8 @@ class ConversationsViewModel @Inject constructor(
) )
) )
} }
}, }
onError = { )
val er = it
throw it
},
onStart = { sendEvent(StartProgressEvent) },
onEnd = { sendEvent(StopProgressEvent) })
} }
fun loadProfileUser() = viewModelScope.launch { fun loadProfileUser() = viewModelScope.launch {
@@ -95,4 +88,4 @@ data class ConversationsLoaded(
val conversations: List<VkConversation>, val conversations: List<VkConversation>,
val profiles: HashMap<Int, VkUser>, val profiles: HashMap<Int, VkUser>,
val groups: HashMap<Int, VkGroup> val groups: HashMap<Int, VkGroup>
) : VKEvent() ) : VkEvent()
@@ -70,7 +70,7 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
binding.loginInput.clearFocus() binding.loginInput.clearFocus()
} }
override fun onEvent(event: VKEvent) { override fun onEvent(event: VkEvent) {
super.onEvent(event) super.onEvent(event)
when (event) { when (event) {
@@ -51,26 +51,25 @@ class LoginViewModel @Inject constructor(
sendEvent(SuccessAuth()) sendEvent(SuccessAuth())
}, },
onError = { onError = {
if (it !is VKException) return@makeJob if (it !is VKException) {
onError(it)
return@makeJob
}
// TODO: 9/27/2021 use `delay` parameter // TODO: 9/27/2021 use `delay` parameter
twoFaCode?.let { sendEvent(CodeSent) } twoFaCode?.let { sendEvent(CodeSent) }
}, }
onStart = { sendEvent(StartProgressEvent) },
onEnd = { sendEvent(StopProgressEvent) }
) )
} }
fun sendSms(validationSid: String) = viewModelScope.launch { fun sendSms(validationSid: String) = viewModelScope.launch {
makeJob({ dataSource.sendSms(validationSid) }, makeJob({ dataSource.sendSms(validationSid) },
onAnswer = { sendEvent(CodeSent) }, onAnswer = { sendEvent(CodeSent) }
onError = {}, )
onStart = {},
onEnd = {})
} }
} }
object CodeSent : VKEvent() object CodeSent : VkEvent()
data class SuccessAuth(val haveAuthorized: Boolean = true) : VKEvent() data class SuccessAuth(val haveAuthorized: Boolean = true) : VkEvent()
@@ -45,6 +45,13 @@ class AttachmentInflater constructor(
private val playColor = ContextCompat.getColor(context, R.color.a3_700) private val playColor = ContextCompat.getColor(context, R.color.a3_700)
private val playBackgroundColor = ContextCompat.getColor(context, R.color.a3_200) private val playBackgroundColor = ContextCompat.getColor(context, R.color.a3_200)
var photoClickListener: ((url: String) -> Unit)? = null
fun setPhotoClickListener(unit: ((url: String) -> Unit)?): AttachmentInflater {
this.photoClickListener = unit
return this
}
fun inflate() { fun inflate() {
if (message.attachments.isNullOrEmpty()) return if (message.attachments.isNullOrEmpty()) return
attachments = message.attachments!! attachments = message.attachments!!
@@ -114,6 +121,12 @@ class AttachmentInflater constructor(
scaleType = ImageView.ScaleType.CENTER_CROP scaleType = ImageView.ScaleType.CENTER_CROP
} }
if (photoClickListener != null) {
newPhoto.setOnClickListener { photoClickListener?.invoke(size.url) }
} else {
newPhoto.setOnClickListener(null)
}
val spacer = Space(context).also { val spacer = Space(context).also {
it.layoutParams = LinearLayoutCompat.LayoutParams( it.layoutParams = LinearLayoutCompat.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
@@ -6,6 +6,7 @@ import android.graphics.drawable.ColorDrawable
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.Toast
import androidx.appcompat.widget.LinearLayoutCompat import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
@@ -16,6 +17,7 @@ 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
import com.meloda.fast.api.model.VkUser import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.model.attachments.VkAttachment
import com.meloda.fast.api.model.attachments.VkPhoto import com.meloda.fast.api.model.attachments.VkPhoto
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
@@ -36,6 +38,8 @@ class MessagesHistoryAdapter constructor(
var onItemClickListener: ((position: Int, view: View) -> Unit)? = null var onItemClickListener: ((position: Int, view: View) -> Unit)? = null
var attachmentClickListener: ((attachment: VkAttachment) -> Unit)? = null
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
when { when {
isPositionHeader(position) -> return HEADER isPositionHeader(position) -> return HEADER
@@ -123,6 +127,8 @@ class MessagesHistoryAdapter constructor(
prevMessage = prevMessage, prevMessage = prevMessage,
nextMessage = nextMessage, nextMessage = nextMessage,
title = binding.title,
avatar = binding.avatar, avatar = binding.avatar,
bubble = binding.bubble, bubble = binding.bubble,
text = binding.text, text = binding.text,
@@ -133,7 +139,9 @@ class MessagesHistoryAdapter constructor(
profiles = profiles, profiles = profiles,
groups = groups groups = groups
).prepare() ).setPhotoClickListener {
Toast.makeText(context, "Photo url: $it", Toast.LENGTH_LONG).show()
}.prepare()
} }
} }
@@ -27,10 +27,9 @@ import com.meloda.fast.api.model.VkUser
import com.meloda.fast.base.BaseViewModelFragment import com.meloda.fast.base.BaseViewModelFragment
import com.meloda.fast.base.viewmodel.StartProgressEvent import com.meloda.fast.base.viewmodel.StartProgressEvent
import com.meloda.fast.base.viewmodel.StopProgressEvent import com.meloda.fast.base.viewmodel.StopProgressEvent
import com.meloda.fast.base.viewmodel.VKEvent import com.meloda.fast.base.viewmodel.VkEvent
import com.meloda.fast.databinding.FragmentMessagesHistoryBinding import com.meloda.fast.databinding.FragmentMessagesHistoryBinding
import com.meloda.fast.extensions.TextViewExtensions.clear import com.meloda.fast.extensions.TextViewExtensions.clear
import com.meloda.fast.extensions.isNotVisible
import com.meloda.fast.util.AndroidUtils import com.meloda.fast.util.AndroidUtils
import com.meloda.fast.util.TimeUtils import com.meloda.fast.util.TimeUtils
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@@ -49,7 +48,7 @@ class MessagesHistoryFragment :
private val action = MutableLiveData<Action>() private val action = MutableLiveData<Action>()
private enum class Action { private enum class Action {
RECORD, SEND RECORD, SEND, EDIT
} }
private val user: VkUser? by lazy { private val user: VkUser? by lazy {
@@ -71,14 +70,15 @@ class MessagesHistoryFragment :
} }
} }
private val replyMessage = MutableLiveData<VkMessage?>()
private val isAttachmentPanelVisible = MutableLiveData(false)
private var timestampTimer: Timer? = null private var timestampTimer: Timer? = null
private lateinit var attachmentController: AttachmentPanelController
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
attachmentController = AttachmentPanelController().init()
val title = when { val title = when {
conversation.isChat() -> conversation.title conversation.isChat() -> conversation.title
conversation.isUser() -> user?.toString() conversation.isUser() -> user?.toString()
@@ -167,8 +167,11 @@ class MessagesHistoryFragment :
it.toString().isNotBlank() it.toString().isNotBlank()
val newValue = val newValue =
if (canSend) Action.SEND when {
else Action.RECORD attachmentController.isEditing -> Action.EDIT
canSend -> Action.SEND
else -> Action.RECORD
}
if (action.value != newValue) action.value = newValue if (action.value != newValue) action.value = newValue
} }
@@ -193,30 +196,21 @@ class MessagesHistoryFragment :
Action.SEND -> { Action.SEND -> {
binding.action.setImageResource(R.drawable.ic_round_send_24) binding.action.setImageResource(R.drawable.ic_round_send_24)
} }
Action.EDIT -> {
binding.action.setImageResource(R.drawable.ic_round_done_24)
}
else -> return@observe else -> return@observe
} }
} }
isAttachmentPanelVisible.observe(viewLifecycleOwner) { attachmentController.isPanelVisible.observe(viewLifecycleOwner) {
val layoutParams = binding.refreshLayout.layoutParams as CoordinatorLayout.LayoutParams val layoutParams = binding.refreshLayout.layoutParams as CoordinatorLayout.LayoutParams
layoutParams.bottomMargin = layoutParams.bottomMargin =
if (it) (binding.attachmentPanel.height / 1.5).roundToInt() else 0 if (it) (binding.attachmentPanel.height / 1.5).roundToInt() else 0
} }
hideAttachmentPanel(duration = 1)
binding.avatar.setOnClickListener {
val isShown = binding.attachmentPanel.isVisible
if (isShown) {
hideAttachmentPanel()
} else {
showAttachmentPanel()
}
}
binding.attachmentPanel.setOnClickListener c@{ binding.attachmentPanel.setOnClickListener c@{
val message = replyMessage.value ?: return@c val message = attachmentController.message.value ?: return@c
val index = adapter.values.indexOf(message) val index = adapter.values.indexOf(message)
if (index == -1) return@c if (index == -1) return@c
@@ -225,9 +219,8 @@ class MessagesHistoryFragment :
} }
binding.dismissReply.setOnClickListener { binding.dismissReply.setOnClickListener {
if (replyMessage.value != null) replyMessage.value = null if (attachmentController.message.value != null)
attachmentController.message.value = null
hideAttachmentPanel()
} }
} }
@@ -282,28 +275,6 @@ class MessagesHistoryFragment :
binding.pin.isVisible = conversation.isPinned binding.pin.isVisible = conversation.isPinned
} }
private fun showAttachmentPanel(duration: Long = 250) {
if (isAttachmentPanelVisible.value == false) isAttachmentPanelVisible.value = true
binding.attachmentPanel.animate()
.translationY(0f)
.alpha(1f)
.setDuration(duration)
.withStartAction { binding.attachmentPanel.isVisible = true }
.start()
}
private fun hideAttachmentPanel(duration: Long = 250) {
if (isAttachmentPanelVisible.value == true) isAttachmentPanelVisible.value = false
binding.attachmentPanel.animate()
.alpha(0f)
.translationY(50f)
.setDuration(duration)
.withEndAction { binding.attachmentPanel.isVisible = false }
.start()
}
private fun performAction() { private fun performAction() {
if (action.value == Action.RECORD) { if (action.value == Action.RECORD) {
return return
@@ -321,7 +292,7 @@ class MessagesHistoryFragment :
fromId = UserConfig.userId, fromId = UserConfig.userId,
date = (date / 1000).toInt(), date = (date / 1000).toInt(),
randomId = 0, randomId = 0,
replyMessage = replyMessage.value replyMessage = attachmentController.message.value
) )
adapter.add(message) adapter.add(message)
@@ -329,10 +300,8 @@ class MessagesHistoryFragment :
binding.recyclerView.smoothScrollToPosition(adapter.lastPosition) binding.recyclerView.smoothScrollToPosition(adapter.lastPosition)
binding.message.clear() binding.message.clear()
val replyMessage = replyMessage.value val replyMessage = attachmentController.message.value
attachmentController.message.value = null
this.replyMessage.value = null
hideAttachmentPanel()
viewModel.sendMessage( viewModel.sendMessage(
peerId = conversation.id, peerId = conversation.id,
@@ -343,12 +312,13 @@ class MessagesHistoryFragment :
} }
} }
override fun onEvent(event: VKEvent) { override fun onEvent(event: VkEvent) {
super.onEvent(event) super.onEvent(event)
when (event) { when (event) {
is MessagesMarkAsImportant -> markMessagesAsImportant(event) is MessagesMarkAsImportant -> markMessagesAsImportant(event)
is MessagesLoaded -> refreshMessages(event) is MessagesLoaded -> refreshMessages(event)
is MessagesPin -> conversation.pinnedMessage = event.message
is StartProgressEvent -> onProgressStarted() is StartProgressEvent -> onProgressStarted()
is StopProgressEvent -> onProgressStopped() is StopProgressEvent -> onProgressStopped()
} }
@@ -436,106 +406,67 @@ class MessagesHistoryFragment :
val message = adapter.values[position] val message = adapter.values[position]
if (message.action != null) return if (message.action != null) return
// val popupMenu = PopupMenu(requireContext(), view) val time = getString(
// R.string.time_format,
// val reply = popupMenu.menu.add( SimpleDateFormat(
// getString(R.string.message_context_action_reply) "dd.MM.yyyy, HH:mm:ss",
// ) Locale.getDefault()
// ).format(message.date * 1000L)
// reply.icon = )
// ContextCompat.getDrawable(
// requireContext(),
// R.drawable.ic_attachment_wall_reply
// )?.constantState?.newDrawable()?.also {
// it.setTint(
// ContextCompat.getColor(
// requireContext(),
// R.color.textColorSecondaryVariant
// )
// )
// }
//
// val important = popupMenu.menu.add(
// getString(
// if (message.important) R.string.message_context_action_unmark_as_important
// else R.string.message_context_action_mark_as_important
// )
// )
//
// important.icon =
// ContextCompat.getDrawable(
// requireContext(),
// R.drawable.ic_star_border
// )?.constantState?.newDrawable()?.also {
// it.setTint(
// ContextCompat.getColor(
// requireContext(),
// R.color.textColorSecondaryVariant
// )
// )
// }
//
// popupMenu.setForceShowIcon(true)
// popupMenu.setOnMenuItemClickListener {
// when (it) {
// reply -> {
// val title = when {
// message.isGroup() && message.group.value != null -> message.group.value?.name
// message.isUser() && message.user.value != null -> message.user.value?.fullName
// else -> null
// }
//
// if (replyMessage.value != message) replyMessage.value = message
//
// binding.replyMessageTitle.text = title
// binding.replyMessageText.text = message.text ?: "[no_message]"
//
// if (binding.attachmentPanel.isNotVisible) binding.avatar.performClick()
// true
// }
//
// important -> {
// viewModel.markAsImportant(
// messagesIds = listOf(message.id),
// important = !message.important
// )
// true
// }
//
// else -> false
// }
// }
// popupMenu.show()
val reply = getString(R.string.message_context_action_reply) val reply = getString(R.string.message_context_action_reply)
val isMessageAlreadyPinned = message.id == conversation.pinnedMessage?.id
val pin = getString(
if (isMessageAlreadyPinned) R.string.message_context_action_unpin
else R.string.message_context_action_pin
)
val edit = getString(R.string.message_context_action_edit)
val important = getString( val important = getString(
if (message.important) R.string.message_context_action_unmark_as_important if (message.important) R.string.message_context_action_unmark_as_important
else R.string.message_context_action_mark_as_important else R.string.message_context_action_mark_as_important
) )
val params = arrayOf(reply, important) val params = mutableListOf<String>()
params.add(reply)
if (conversation.canChangePin) {
params.add(pin)
}
if (message.canEdit()) {
params.add(edit)
}
params.add(important)
val arrayParams = params.toTypedArray()
val dialog = MaterialAlertDialogBuilder(requireContext()) val dialog = MaterialAlertDialogBuilder(requireContext())
.setItems(params) { _, which -> .setTitle(time)
.setItems(arrayParams) { _, which ->
when (params[which]) { when (params[which]) {
important -> viewModel.markAsImportant( important -> viewModel.markAsImportant(
messagesIds = listOf(message.id), messagesIds = listOf(message.id),
important = !message.important important = !message.important
) )
reply -> { reply -> {
val title = when { if (attachmentController.message.value != message)
message.isGroup() && message.group.value != null -> message.group.value?.name attachmentController.message.value = message
message.isUser() && message.user.value != null -> message.user.value?.fullName }
else -> null pin -> viewModel.pinMessage(
} peerId = conversation.id,
messageId = message.id,
pin = !isMessageAlreadyPinned
)
edit -> {
attachmentController.isEditing = true
if (replyMessage.value != message) replyMessage.value = message if (attachmentController.message.value != message)
attachmentController.message.value = message
binding.replyMessageTitle.text = title
binding.replyMessageText.text = message.text ?: "[no_message]"
if (binding.attachmentPanel.isNotVisible) binding.avatar.performClick()
} }
} }
} }
@@ -549,4 +480,78 @@ class MessagesHistoryFragment :
return true return true
} }
private inner class AttachmentPanelController {
val isPanelVisible = MutableLiveData(false)
val message = MutableLiveData<VkMessage?>()
var isEditing = false
fun init(): AttachmentPanelController {
message.observe(viewLifecycleOwner) { value ->
if (value != null) {
applyMessage(value)
} else {
clearMessage()
}
}
message.value = null
return this
}
private fun applyMessage(message: VkMessage) {
showPanel()
val title = when {
message.isGroup() && message.group.value != null -> message.group.value?.name
message.isUser() && message.user.value != null -> message.user.value?.fullName
else -> null
}
binding.replyMessageTitle.text = title
binding.replyMessageText.text = message.text ?: "[no_message]"
if (isEditing) {
binding.message.setText(message.text ?: "[no_message]")
}
}
private fun clearMessage() {
hidePanel()
binding.replyMessageTitle.clear()
binding.replyMessageText.clear()
if (isEditing) {
isEditing = false
binding.message.clear()
}
}
private fun showPanel(duration: Long = 250) {
if (attachmentController.isPanelVisible.value == false)
attachmentController.isPanelVisible.value = true
binding.attachmentPanel.animate()
.translationY(0f)
.alpha(1f)
.setDuration(duration)
.withStartAction { binding.attachmentPanel.isVisible = true }
.start()
}
private fun hidePanel(duration: Long = 250) {
if (attachmentController.isPanelVisible.value == true)
attachmentController.isPanelVisible.value = false
binding.attachmentPanel.animate()
.alpha(0f)
.translationY(50f)
.setDuration(duration)
.withEndAction { binding.attachmentPanel.isVisible = false }
.start()
}
}
} }
@@ -6,28 +6,24 @@ 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
import com.meloda.fast.api.model.VkUser import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.model.request.MessagesGetHistoryRequest import com.meloda.fast.api.model.request.*
import com.meloda.fast.api.model.request.MessagesMarkAsImportantRequest
import com.meloda.fast.api.model.request.MessagesSendRequest
import com.meloda.fast.api.network.datasource.MessagesDataSource import com.meloda.fast.api.network.datasource.MessagesDataSource
import com.meloda.fast.base.viewmodel.BaseViewModel import com.meloda.fast.base.viewmodel.BaseViewModel
import com.meloda.fast.base.viewmodel.StartProgressEvent import com.meloda.fast.base.viewmodel.VkEvent
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
@HiltViewModel @HiltViewModel
class MessagesHistoryViewModel @Inject constructor( class MessagesHistoryViewModel @Inject constructor(
private val dataSource: MessagesDataSource private val messages: MessagesDataSource
) : BaseViewModel() { ) : BaseViewModel() {
fun loadHistory( fun loadHistory(
peerId: Int peerId: Int
) = viewModelScope.launch { ) = viewModelScope.launch {
makeJob({ makeJob({
dataSource.getHistory( messages.getHistory(
MessagesGetHistoryRequest( MessagesGetHistoryRequest(
count = 30, count = 30,
peerId = peerId, peerId = peerId,
@@ -53,18 +49,18 @@ class MessagesHistoryViewModel @Inject constructor(
} }
} }
val messages = hashMapOf<Int, VkMessage>() val hashMessages = hashMapOf<Int, VkMessage>()
response.items.forEach { baseMessage -> response.items.forEach { baseMessage ->
baseMessage.asVkMessage().let { message -> messages[message.id] = message } baseMessage.asVkMessage().let { message -> hashMessages[message.id] = message }
} }
dataSource.storeMessages(messages.values.toList()) messages.store(hashMessages.values.toList())
val conversations = hashMapOf<Int, VkConversation>() val conversations = hashMapOf<Int, VkConversation>()
response.conversations?.let { baseConversations -> response.conversations?.let { baseConversations ->
baseConversations.forEach { baseConversation -> baseConversations.forEach { baseConversation ->
baseConversation.asVkConversation( baseConversation.asVkConversation(
messages[baseConversation.last_message_id] hashMessages[baseConversation.last_message_id]
).let { conversation -> conversations[conversation.id] = conversation } ).let { conversation -> conversations[conversation.id] = conversation }
} }
} }
@@ -75,16 +71,10 @@ class MessagesHistoryViewModel @Inject constructor(
profiles = profiles, profiles = profiles,
groups = groups, groups = groups,
conversations = conversations, conversations = conversations,
messages = messages.values.toList() messages = hashMessages.values.toList()
) )
) )
}, })
onError = {
val throwable = it
throw it
},
onStart = { sendEvent(StartProgressEvent) },
onEnd = { sendEvent(StopProgressEvent) })
} }
fun sendMessage( fun sendMessage(
@@ -96,7 +86,7 @@ class MessagesHistoryViewModel @Inject constructor(
) = viewModelScope.launch { ) = viewModelScope.launch {
makeJob( makeJob(
{ {
dataSource.send( messages.send(
MessagesSendRequest( MessagesSendRequest(
peerId = peerId, peerId = peerId,
randomId = randomId, randomId = randomId,
@@ -108,10 +98,6 @@ class MessagesHistoryViewModel @Inject constructor(
onAnswer = { onAnswer = {
val response = it.response ?: return@makeJob val response = it.response ?: return@makeJob
setId?.invoke(response) setId?.invoke(response)
},
onError = {
val throwable = it
val i = 0
}) })
} }
@@ -120,7 +106,7 @@ class MessagesHistoryViewModel @Inject constructor(
important: Boolean important: Boolean
) = viewModelScope.launch { ) = viewModelScope.launch {
makeJob({ makeJob({
dataSource.markAsImportant( messages.markAsImportant(
MessagesMarkAsImportantRequest( MessagesMarkAsImportantRequest(
messagesIds = messagesIds, messagesIds = messagesIds,
important = important important = important
@@ -135,13 +121,39 @@ class MessagesHistoryViewModel @Inject constructor(
important = important important = important
) )
) )
},
onError = {
val throwable = it
val i = 0
}) })
} }
fun pinMessage(
peerId: Int,
messageId: Int? = null,
conversationMessageId: Int? = null,
pin: Boolean
) = viewModelScope.launch {
if (pin) {
makeJob({
messages.pin(
MessagesPinMessageRequest(
peerId = peerId,
messageId = messageId,
conversationMessageId = conversationMessageId
)
)
},
onAnswer = {
val response = it.response ?: return@makeJob
sendEvent(MessagesPin(response.asVkMessage()))
}
)
} else {
makeJob({ messages.unpin(MessagesUnPinMessageRequest(peerId = peerId)) },
onAnswer = {
println("Fast::MessagesHistoryViewModel::unPin::Response::${it.response}")
sendEvent(MessagesUnpin)
}
)
}
}
} }
data class MessagesLoaded( data class MessagesLoaded(
@@ -150,9 +162,16 @@ data class MessagesLoaded(
val messages: List<VkMessage>, val messages: List<VkMessage>,
val profiles: HashMap<Int, VkUser>, val profiles: HashMap<Int, VkUser>,
val groups: HashMap<Int, VkGroup> val groups: HashMap<Int, VkGroup>
) : VKEvent() ) : VkEvent()
data class MessagesMarkAsImportant( data class MessagesMarkAsImportant(
val messagesIds: List<Int>, val messagesIds: List<Int>,
val important: Boolean val important: Boolean
) : VKEvent() ) : VkEvent()
data class MessagesPin(
val message: VkMessage
) : VkEvent()
object MessagesUnpin : VkEvent()
@@ -9,7 +9,6 @@ import android.widget.Space
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.widget.LinearLayoutCompat import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.load import coil.load
import com.meloda.fast.R import com.meloda.fast.R
@@ -65,14 +64,17 @@ class MessagesPreparator constructor(
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background) ContextCompat.getDrawable(context, R.drawable.ic_message_out_background)
private val backgroundMiddleOut = private val backgroundMiddleOut =
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle) ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle)
// private val backgroundStrokeOut =
// ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_stroke)
// private val backgroundMiddleStrokeOut =
// ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle_stroke)
private val rootHighlightedColor = private val rootHighlightedColor =
ContextCompat.getColor(context, R.color.n2_100) ContextCompat.getColor(context, R.color.n2_100)
private var photoClickListener: ((url: String) -> Unit)? = null
fun setPhotoClickListener(unit: ((url: String) -> Unit)?): MessagesPreparator {
this.photoClickListener = unit
return this
}
fun prepare() { fun prepare() {
val messageUser: VkUser? = (if (message.isUser()) { val messageUser: VkUser? = (if (message.isUser()) {
profiles[message.fromId] profiles[message.fromId]
@@ -104,20 +106,24 @@ class MessagesPreparator constructor(
) )
if (message.isPeerChat()) { if (message.isPeerChat()) {
val prevSenderDiff = VkUtils.isPreviousMessageFromDifferentSender(prevMessage, message)
val fromDiffSender = VkUtils.isPreviousMessageFromDifferentSender(prevMessage, message) val nextSenderDiff = VkUtils.isPreviousMessageFromDifferentSender(message, nextMessage)
val fiveMinAgo = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message) val fiveMinAgo = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message)
val change = (prevMessage?.date ?: 0) - message.date val change = (prevMessage?.date ?: 0) - message.date
Log.d( Log.d(
"Fast::MessagesPreparator", "Fast::MessagesPreparator",
"text: ${message.text}; prevText: ${prevMessage?.text}; time change: $change; fromDiffSender: $fromDiffSender; fiveMinAgo: $fiveMinAgo; " "text: ${message.text}; prevText: ${prevMessage?.text}; time change: $change; fromDiffSender: $prevSenderDiff; fiveMinAgo: $fiveMinAgo; "
) )
title?.isVisible = fromDiffSender || fiveMinAgo title?.isVisible = prevSenderDiff || fiveMinAgo
avatar?.isInvisible = fromDiffSender && fiveMinAgo avatar?.visibility =
if (nextSenderDiff
|| (fiveMinAgo && prevSenderDiff)
|| (!prevSenderDiff && nextMessage == null)
) View.VISIBLE else View.INVISIBLE
} else { } else {
title?.isVisible = false title?.isVisible = false
avatar?.isVisible = false avatar?.isVisible = false
@@ -131,7 +137,6 @@ class MessagesPreparator constructor(
} }
title.text = titleString title.text = titleString
title.measure(0, 0)
} }
} }
@@ -164,13 +169,16 @@ class MessagesPreparator constructor(
attachmentContainer.removeAllViews() attachmentContainer.removeAllViews()
} else { } else {
attachmentContainer.isVisible = true attachmentContainer.isVisible = true
AttachmentInflater( AttachmentInflater(
context = context, context = context,
container = attachmentContainer, container = attachmentContainer,
message = message, message = message,
groups = groups, groups = groups,
profiles = profiles profiles = profiles
).inflate() )
.setPhotoClickListener(photoClickListener)
.inflate()
} }
} }
} }
@@ -0,0 +1,48 @@
package com.meloda.fast.screens.photos
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.fragment.app.viewModels
import com.meloda.fast.base.BaseViewModelFragment
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class PhotoViewFragment : BaseViewModelFragment<PhotoViewViewModel>() {
override val viewModel: PhotoViewViewModel by viewModels()
// private val photosList: MutableList<VkPhoto> = mutableListOf()
private var photoLink: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
photoLink = requireArguments().getString("photoLink")
// val list: List<*>? = Gson().fromJson(
// requireArguments().getString("photosList"),
// List::class.java
// )
//
// list?.forEach { if (it is VkPhoto) photosList.add(it) }
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ImageView(requireContext())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
photoLink?.let { viewModel.loadImageFromUrl(it, requireView() as ImageView) }
}
}
@@ -0,0 +1,22 @@
package com.meloda.fast.screens.photos
import android.widget.ImageView
import androidx.lifecycle.viewModelScope
import coil.load
import com.meloda.fast.base.viewmodel.BaseViewModel
import kotlinx.coroutines.launch
class PhotoViewViewModel : BaseViewModel() {
fun loadImageFromUrl(
url: String,
imageView: ImageView
) = viewModelScope.launch {
imageView.load(url)
}
fun saveImageToLocalStorage(url: String) = viewModelScope.launch {
TODO("Not implemented")
}
}
@@ -7,6 +7,8 @@ import java.util.*
object TimeUtils { object TimeUtils {
const val ONE_DAY_IN_SECONDS = 86400
fun removeTime(date: Date): Long { fun removeTime(date: Date): Long {
return Calendar.getInstance().apply { return Calendar.getInstance().apply {
time = date time = date
@@ -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="M9,16.2l-3.5,-3.5c-0.39,-0.39 -1.01,-0.39 -1.4,0 -0.39,0.39 -0.39,1.01 0,1.4l4.19,4.19c0.39,0.39 1.02,0.39 1.41,0L20.3,7.7c0.39,-0.39 0.39,-1.01 0,-1.4 -0.39,-0.39 -1.01,-0.39 -1.4,0L9,16.2z" />
</vector>
@@ -219,8 +219,10 @@
android:minHeight="105dp" android:minHeight="105dp"
android:orientation="vertical" android:orientation="vertical"
android:padding="16dp" android:padding="16dp"
android:visibility="gone"
app:layout_anchor="@+id/messagePanel" app:layout_anchor="@+id/messagePanel"
app:layout_anchorGravity="center_vertical|top"> app:layout_anchorGravity="center_vertical|top"
tools:visibility="visible">
<androidx.appcompat.widget.LinearLayoutCompat <androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/replyMessage" android:id="@+id/replyMessage"
@@ -249,10 +251,9 @@
android:id="@+id/dismissReply" android:id="@+id/dismissReply"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/ic_image_button_circle_background" android:background="?selectableItemBackgroundBorderless"
android:backgroundTint="@color/n1_50"
android:src="@drawable/ic_round_close_20" android:src="@drawable/ic_round_close_20"
android:tint="?colorSecondary3" /> android:tint="@color/n1_800" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
+14 -13
View File
@@ -70,19 +70,6 @@
android:textColor="@color/n1_800" android:textColor="@color/n1_800"
tools:text="This" /> tools:text="This" />
<Space
android:id="@+id/attachmentSpacer"
android:layout_width="wrap_content"
android:layout_height="5dp"
android:visibility="gone" />
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/attachmentContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</com.meloda.fast.widget.BoundedLinearLayout> </com.meloda.fast.widget.BoundedLinearLayout>
@@ -96,6 +83,20 @@
android:src="@color/a3_200" /> android:src="@color/a3_200" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
<Space
android:id="@+id/attachmentSpacer"
android:layout_width="wrap_content"
android:layout_height="5dp"
android:visibility="gone" />
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/attachmentContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</layout> </layout>
+24 -33
View File
@@ -1,6 +1,5 @@
<?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<androidx.appcompat.widget.LinearLayoutCompat <androidx.appcompat.widget.LinearLayoutCompat
@@ -35,44 +34,36 @@
android:id="@+id/bubble" 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" android:layout_gravity="end"
android:background="@drawable/ic_message_out_background" android:background="@drawable/ic_message_out_background"
android:clipChildren="true"
android:clipToPadding="true"
android:orientation="vertical"> android:orientation="vertical">
<RelativeLayout <com.google.android.material.textview.MaterialTextView
android:id="@+id/text"
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:padding="15dp"
android:textColor="@color/n1_900"
tools:text="This is test" />
<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_900"
tools:text="This is test" />
<Space
android:id="@+id/attachmentSpacer"
android:layout_width="wrap_content"
android:layout_height="5dp"
android:layout_below="@+id/text"
android:visibility="gone" />
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/attachmentContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/attachmentSpacer"
android:orientation="vertical"
android:visibility="gone"
app:layout_anchor="@+id/text"
app:layout_anchorGravity="bottom" />
</RelativeLayout>
</com.meloda.fast.widget.BoundedLinearLayout> </com.meloda.fast.widget.BoundedLinearLayout>
<Space
android:id="@+id/attachmentSpacer"
android:layout_width="wrap_content"
android:layout_height="5dp"
android:layout_below="@+id/text"
android:visibility="gone" />
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/attachmentContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:orientation="vertical"
android:visibility="gone" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</layout> </layout>
+12 -1
View File
@@ -21,6 +21,17 @@
android:id="@+id/messagesHistoryFragment" android:id="@+id/messagesHistoryFragment"
android:name="com.meloda.fast.screens.messages.MessagesHistoryFragment" android:name="com.meloda.fast.screens.messages.MessagesHistoryFragment"
android:label="MessagesHistoryFragment" android:label="MessagesHistoryFragment"
tools:layout="@layout/fragment_messages_history" /> tools:layout="@layout/fragment_messages_history">
<action
android:id="@+id/toPhotoView"
app:destination="@+id/photoViewFragment" />
</fragment>
<fragment
android:id="@+id/photoViewFragment"
android:name="com.meloda.fast.screens.photos.PhotoViewFragment"
android:label="PhotoViewFragment" />
</navigation> </navigation>
+4
View File
@@ -117,4 +117,8 @@
<string name="message_context_action_reply">Reply</string> <string name="message_context_action_reply">Reply</string>
<string name="message_context_action_mark_as_important">Mark as important</string> <string name="message_context_action_mark_as_important">Mark as important</string>
<string name="message_context_action_unmark_as_important">Unmark as important</string> <string name="message_context_action_unmark_as_important">Unmark as important</string>
<string name="time_format">Time: %s</string>
<string name="message_context_action_pin">Pin</string>
<string name="message_context_action_unpin">Unpin</string>
<string name="message_context_action_edit">Edit</string>
</resources> </resources>