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
data class VkConversation(
@PrimaryKey(autoGenerate = false)
val id: Int,
val ownerId: Int?,
val title: String?,
val photo200: String?,
val type: String,
val callInProgress: Boolean,
val isPhantom: Boolean,
val lastConversationMessageId: Int,
val inRead: Int,
val outRead: Int,
val isMarkedUnread: Boolean,
val lastMessageId: Int,
val unreadCount: Int?,
val membersCount: Int?,
val isPinned: Boolean,
var id: Int,
var ownerId: Int?,
var title: String?,
var photo200: String?,
var type: String,
var callInProgress: Boolean,
var isPhantom: Boolean,
var lastConversationMessageId: Int,
var inRead: Int,
var outRead: Int,
var isMarkedUnread: Boolean,
var lastMessageId: Int,
var unreadCount: Int?,
var membersCount: Int?,
var isPinned: Boolean,
var canChangePin: Boolean,
@Embedded(prefix = "pinnedMessage_")
var pinnedMessage: VkMessage? = null,
@Embedded(prefix = "lastMessage_")
var lastMessage: VkMessage? = null
var lastMessage: VkMessage? = null,
) : Parcelable {
fun isChat() = type == "chat"
@@ -4,8 +4,10 @@ import androidx.lifecycle.MutableLiveData
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.model.attachments.VkAttachment
import com.meloda.fast.base.adapter.SelectableItem
import com.meloda.fast.util.TimeUtils
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@@ -58,6 +60,10 @@ data class VkMessage(
return Action.parse(action)
}
fun canEdit() =
fromId == UserConfig.userId &&
(System.currentTimeMillis() / 1000 - date.toLong() < TimeUtils.ONE_DAY_IN_SECONDS)
fun copyMessage(
id: Int = this.id,
text: String? = this.text,
@@ -40,7 +40,8 @@ data class BaseVkConversation(
unreadCount = unread_count,
membersCount = chat_settings?.members_count,
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 {
this.lastMessage = lastMessage
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
data class MessagesGetLongPollServerRequest(
val needPts: Boolean,
@@ -1,6 +1,6 @@
package com.meloda.fast.api.network
object VKUrls {
object VkUrls {
const val OAUTH = "https://oauth.vk.com"
const val API = "https://api.vk.com/method"
@@ -22,6 +22,8 @@ object VKUrls {
const val GetHistory = "$API/messages.getHistory"
const val Send = "$API/messages.send"
const val MarkAsImportant = "$API/messages.markAsImportant"
const val Pin = "$API/messages.pin"
const val Unpin = "$API/messages.unpin"
const val GetLongPollServer = "$API/messages.getLongPollServer"
const val GetLongPollHistory = "$API/messages.getLongPollHistory"
}
@@ -1,10 +1,7 @@
package com.meloda.fast.api.network.datasource
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.request.MessagesGetHistoryRequest
import com.meloda.fast.api.model.request.MessagesGetLongPollServerRequest
import com.meloda.fast.api.model.request.MessagesMarkAsImportantRequest
import com.meloda.fast.api.model.request.MessagesSendRequest
import com.meloda.fast.api.model.request.*
import com.meloda.fast.api.network.repo.MessagesRepo
import com.meloda.fast.database.dao.MessagesDao
import javax.inject.Inject
@@ -26,8 +23,14 @@ class MessagesDataSource @Inject constructor(
suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) =
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
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.network.Answer
import com.meloda.fast.api.model.response.ResponseSendSms
@@ -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>
}
@@ -2,7 +2,7 @@ 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.VkUrls
import com.meloda.fast.api.model.response.ConversationsGetResponse
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
@@ -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>>
}
@@ -2,9 +2,10 @@ package com.meloda.fast.api.network.repo
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.model.base.BaseVkLongPoll
import com.meloda.fast.api.model.base.BaseVkMessage
import com.meloda.fast.api.model.response.MessagesGetHistoryResponse
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.FormUrlEncoded
import retrofit2.http.POST
@@ -12,19 +13,27 @@ import retrofit2.http.POST
interface MessagesRepo {
@FormUrlEncoded
@POST(VKUrls.Messages.GetHistory)
@POST(VkUrls.Messages.GetHistory)
suspend fun getHistory(@FieldMap params: Map<String, String>): Answer<ApiResponse<MessagesGetHistoryResponse>>
@FormUrlEncoded
@POST(VKUrls.Messages.Send)
@POST(VkUrls.Messages.Send)
suspend fun send(@FieldMap params: Map<String, String>): Answer<ApiResponse<Int>>
@FormUrlEncoded
@POST(VKUrls.Messages.MarkAsImportant)
@POST(VkUrls.Messages.MarkAsImportant)
suspend fun markAsImportant(@FieldMap params: Map<String, String>): Answer<ApiResponse<List<Int>>>
@FormUrlEncoded
@POST(VKUrls.Messages.GetLongPollServer)
@POST(VkUrls.Messages.GetLongPollServer)
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.model.base.BaseVkUser
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.FormUrlEncoded
import retrofit2.http.POST
@@ -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>>>
@@ -11,7 +11,7 @@ import com.meloda.fast.activity.MainActivity
import com.meloda.fast.api.UserConfig
import com.meloda.fast.base.viewmodel.BaseViewModel
import com.meloda.fast.base.viewmodel.IllegalTokenEvent
import com.meloda.fast.base.viewmodel.VKEvent
import com.meloda.fast.base.viewmodel.VkEvent
import kotlinx.coroutines.flow.collect
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) {
Toast.makeText(
requireContext(), R.string.authorization_failed, Toast.LENGTH_LONG
@@ -15,7 +15,7 @@ abstract class BaseViewModel : ViewModel() {
var unknownErrorDefaultText: String = ""
protected val tasksEventChannel = Channel<VKEvent>()
protected val tasksEventChannel = Channel<VkEvent>()
val tasksEvent = tasksEventChannel.receiveAsFlow()
protected fun <T> makeJob(
@@ -25,22 +25,35 @@ abstract class BaseViewModel : ViewModel() {
onEnd: (suspend () -> Unit)? = null,
onError: (suspend (Throwable) -> Unit)? = null
) = viewModelScope.launch {
onStart?.invoke()
onStart?.invoke() ?: onStart()
when (val response = job()) {
is Answer.Success -> onAnswer(response.data)
is Answer.Error -> {
checkErrors(response.throwable)
onError?.invoke(response.throwable) ?: sendEvent(
ErrorEvent(
response.throwable.message
?: unknownErrorDefaultText
)
)
onError?.invoke(response.throwable) ?: onError(response.throwable)
}
}
}.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) {
when (throwable) {
@@ -5,15 +5,15 @@ data class ShowDialogInfoEvent(
val message: String,
val positiveBtn: 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()
data class CaptchaEvent(val sid: String, val image: String) : VKEvent()
data class ValidationEvent(val sid: String) : VKEvent()
object IllegalTokenEvent : VkEvent()
data class CaptchaEvent(val sid: String, val image: String) : VkEvent()
data class ValidationEvent(val sid: String) : VkEvent()
object StartProgressEvent : VKEvent()
object StopProgressEvent : VKEvent()
object StartProgressEvent : VkEvent()
object StopProgressEvent : VkEvent()
abstract class VKEvent
abstract class VkEvent
@@ -19,7 +19,7 @@ import com.meloda.fast.database.dao.UsersDao
VkUser::class,
VkGroup::class
],
version = 25,
version = 26,
exportSchema = false,
)
@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.viewmodel.StartProgressEvent
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.AppSettings
import com.meloda.fast.common.dataStore
@@ -185,7 +185,7 @@ class ConversationsFragment :
.show()
}
override fun onEvent(event: VKEvent) {
override fun onEvent(event: VkEvent) {
super.onEvent(event)
when (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.UsersDataSource
import com.meloda.fast.base.viewmodel.BaseViewModel
import com.meloda.fast.base.viewmodel.StartProgressEvent
import com.meloda.fast.base.viewmodel.StopProgressEvent
import com.meloda.fast.base.viewmodel.VKEvent
import com.meloda.fast.base.viewmodel.VkEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
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 {
@@ -95,4 +88,4 @@ data class ConversationsLoaded(
val conversations: List<VkConversation>,
val profiles: HashMap<Int, VkUser>,
val groups: HashMap<Int, VkGroup>
) : VKEvent()
) : VkEvent()
@@ -70,7 +70,7 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
binding.loginInput.clearFocus()
}
override fun onEvent(event: VKEvent) {
override fun onEvent(event: VkEvent) {
super.onEvent(event)
when (event) {
@@ -51,26 +51,25 @@ class LoginViewModel @Inject constructor(
sendEvent(SuccessAuth())
},
onError = {
if (it !is VKException) return@makeJob
if (it !is VKException) {
onError(it)
return@makeJob
}
// TODO: 9/27/2021 use `delay` parameter
twoFaCode?.let { sendEvent(CodeSent) }
},
onStart = { sendEvent(StartProgressEvent) },
onEnd = { sendEvent(StopProgressEvent) }
}
)
}
fun sendSms(validationSid: String) = viewModelScope.launch {
makeJob({ dataSource.sendSms(validationSid) },
onAnswer = { sendEvent(CodeSent) },
onError = {},
onStart = {},
onEnd = {})
onAnswer = { sendEvent(CodeSent) }
)
}
}
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 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() {
if (message.attachments.isNullOrEmpty()) return
attachments = message.attachments!!
@@ -114,6 +121,12 @@ class AttachmentInflater constructor(
scaleType = ImageView.ScaleType.CENTER_CROP
}
if (photoClickListener != null) {
newPhoto.setOnClickListener { photoClickListener?.invoke(size.url) }
} else {
newPhoto.setOnClickListener(null)
}
val spacer = Space(context).also {
it.layoutParams = LinearLayoutCompat.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
@@ -6,6 +6,7 @@ import android.graphics.drawable.ColorDrawable
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.Toast
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.view.isVisible
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.VkMessage
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.base.adapter.BaseAdapter
import com.meloda.fast.base.adapter.BaseHolder
@@ -36,6 +38,8 @@ class MessagesHistoryAdapter constructor(
var onItemClickListener: ((position: Int, view: View) -> Unit)? = null
var attachmentClickListener: ((attachment: VkAttachment) -> Unit)? = null
override fun getItemViewType(position: Int): Int {
when {
isPositionHeader(position) -> return HEADER
@@ -123,6 +127,8 @@ class MessagesHistoryAdapter constructor(
prevMessage = prevMessage,
nextMessage = nextMessage,
title = binding.title,
avatar = binding.avatar,
bubble = binding.bubble,
text = binding.text,
@@ -133,7 +139,9 @@ class MessagesHistoryAdapter constructor(
profiles = profiles,
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.viewmodel.StartProgressEvent
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.extensions.TextViewExtensions.clear
import com.meloda.fast.extensions.isNotVisible
import com.meloda.fast.util.AndroidUtils
import com.meloda.fast.util.TimeUtils
import dagger.hilt.android.AndroidEntryPoint
@@ -49,7 +48,7 @@ class MessagesHistoryFragment :
private val action = MutableLiveData<Action>()
private enum class Action {
RECORD, SEND
RECORD, SEND, EDIT
}
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 lateinit var attachmentController: AttachmentPanelController
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
attachmentController = AttachmentPanelController().init()
val title = when {
conversation.isChat() -> conversation.title
conversation.isUser() -> user?.toString()
@@ -167,8 +167,11 @@ class MessagesHistoryFragment :
it.toString().isNotBlank()
val newValue =
if (canSend) Action.SEND
else Action.RECORD
when {
attachmentController.isEditing -> Action.EDIT
canSend -> Action.SEND
else -> Action.RECORD
}
if (action.value != newValue) action.value = newValue
}
@@ -193,30 +196,21 @@ class MessagesHistoryFragment :
Action.SEND -> {
binding.action.setImageResource(R.drawable.ic_round_send_24)
}
Action.EDIT -> {
binding.action.setImageResource(R.drawable.ic_round_done_24)
}
else -> return@observe
}
}
isAttachmentPanelVisible.observe(viewLifecycleOwner) {
attachmentController.isPanelVisible.observe(viewLifecycleOwner) {
val layoutParams = binding.refreshLayout.layoutParams as CoordinatorLayout.LayoutParams
layoutParams.bottomMargin =
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@{
val message = replyMessage.value ?: return@c
val message = attachmentController.message.value ?: return@c
val index = adapter.values.indexOf(message)
if (index == -1) return@c
@@ -225,9 +219,8 @@ class MessagesHistoryFragment :
}
binding.dismissReply.setOnClickListener {
if (replyMessage.value != null) replyMessage.value = null
hideAttachmentPanel()
if (attachmentController.message.value != null)
attachmentController.message.value = null
}
}
@@ -282,28 +275,6 @@ class MessagesHistoryFragment :
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() {
if (action.value == Action.RECORD) {
return
@@ -321,7 +292,7 @@ class MessagesHistoryFragment :
fromId = UserConfig.userId,
date = (date / 1000).toInt(),
randomId = 0,
replyMessage = replyMessage.value
replyMessage = attachmentController.message.value
)
adapter.add(message)
@@ -329,10 +300,8 @@ class MessagesHistoryFragment :
binding.recyclerView.smoothScrollToPosition(adapter.lastPosition)
binding.message.clear()
val replyMessage = replyMessage.value
this.replyMessage.value = null
hideAttachmentPanel()
val replyMessage = attachmentController.message.value
attachmentController.message.value = null
viewModel.sendMessage(
peerId = conversation.id,
@@ -343,12 +312,13 @@ class MessagesHistoryFragment :
}
}
override fun onEvent(event: VKEvent) {
override fun onEvent(event: VkEvent) {
super.onEvent(event)
when (event) {
is MessagesMarkAsImportant -> markMessagesAsImportant(event)
is MessagesLoaded -> refreshMessages(event)
is MessagesPin -> conversation.pinnedMessage = event.message
is StartProgressEvent -> onProgressStarted()
is StopProgressEvent -> onProgressStopped()
}
@@ -436,106 +406,67 @@ class MessagesHistoryFragment :
val message = adapter.values[position]
if (message.action != null) return
// val popupMenu = PopupMenu(requireContext(), view)
//
// val reply = popupMenu.menu.add(
// getString(R.string.message_context_action_reply)
// )
//
// 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 time = getString(
R.string.time_format,
SimpleDateFormat(
"dd.MM.yyyy, HH:mm:ss",
Locale.getDefault()
).format(message.date * 1000L)
)
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(
if (message.important) R.string.message_context_action_unmark_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())
.setItems(params) { _, which ->
.setTitle(time)
.setItems(arrayParams) { _, which ->
when (params[which]) {
important -> viewModel.markAsImportant(
messagesIds = listOf(message.id),
important = !message.important
)
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 (attachmentController.message.value != message)
attachmentController.message.value = message
}
pin -> viewModel.pinMessage(
peerId = conversation.id,
messageId = message.id,
pin = !isMessageAlreadyPinned
)
edit -> {
attachmentController.isEditing = true
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()
if (attachmentController.message.value != message)
attachmentController.message.value = message
}
}
}
@@ -549,4 +480,78 @@ class MessagesHistoryFragment :
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.VkMessage
import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.model.request.MessagesGetHistoryRequest
import com.meloda.fast.api.model.request.MessagesMarkAsImportantRequest
import com.meloda.fast.api.model.request.MessagesSendRequest
import com.meloda.fast.api.model.request.*
import com.meloda.fast.api.network.datasource.MessagesDataSource
import com.meloda.fast.base.viewmodel.BaseViewModel
import com.meloda.fast.base.viewmodel.StartProgressEvent
import com.meloda.fast.base.viewmodel.StopProgressEvent
import com.meloda.fast.base.viewmodel.VKEvent
import com.meloda.fast.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
private val messages: MessagesDataSource
) : BaseViewModel() {
fun loadHistory(
peerId: Int
) = viewModelScope.launch {
makeJob({
dataSource.getHistory(
messages.getHistory(
MessagesGetHistoryRequest(
count = 30,
peerId = peerId,
@@ -53,18 +49,18 @@ class MessagesHistoryViewModel @Inject constructor(
}
}
val messages = hashMapOf<Int, VkMessage>()
val hashMessages = hashMapOf<Int, VkMessage>()
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>()
response.conversations?.let { baseConversations ->
baseConversations.forEach { baseConversation ->
baseConversation.asVkConversation(
messages[baseConversation.last_message_id]
hashMessages[baseConversation.last_message_id]
).let { conversation -> conversations[conversation.id] = conversation }
}
}
@@ -75,16 +71,10 @@ class MessagesHistoryViewModel @Inject constructor(
profiles = profiles,
groups = groups,
conversations = conversations,
messages = messages.values.toList()
messages = hashMessages.values.toList()
)
)
},
onError = {
val throwable = it
throw it
},
onStart = { sendEvent(StartProgressEvent) },
onEnd = { sendEvent(StopProgressEvent) })
})
}
fun sendMessage(
@@ -96,7 +86,7 @@ class MessagesHistoryViewModel @Inject constructor(
) = viewModelScope.launch {
makeJob(
{
dataSource.send(
messages.send(
MessagesSendRequest(
peerId = peerId,
randomId = randomId,
@@ -108,10 +98,6 @@ class MessagesHistoryViewModel @Inject constructor(
onAnswer = {
val response = it.response ?: return@makeJob
setId?.invoke(response)
},
onError = {
val throwable = it
val i = 0
})
}
@@ -120,7 +106,7 @@ class MessagesHistoryViewModel @Inject constructor(
important: Boolean
) = viewModelScope.launch {
makeJob({
dataSource.markAsImportant(
messages.markAsImportant(
MessagesMarkAsImportantRequest(
messagesIds = messagesIds,
important = important
@@ -135,13 +121,39 @@ class MessagesHistoryViewModel @Inject constructor(
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(
@@ -150,9 +162,16 @@ data class MessagesLoaded(
val messages: List<VkMessage>,
val profiles: HashMap<Int, VkUser>,
val groups: HashMap<Int, VkGroup>
) : VKEvent()
) : VkEvent()
data class MessagesMarkAsImportant(
val messagesIds: List<Int>,
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 androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.content.ContextCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import coil.load
import com.meloda.fast.R
@@ -65,14 +64,17 @@ class MessagesPreparator constructor(
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background)
private val backgroundMiddleOut =
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 =
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() {
val messageUser: VkUser? = (if (message.isUser()) {
profiles[message.fromId]
@@ -104,20 +106,24 @@ class MessagesPreparator constructor(
)
if (message.isPeerChat()) {
val fromDiffSender = VkUtils.isPreviousMessageFromDifferentSender(prevMessage, message)
val prevSenderDiff = VkUtils.isPreviousMessageFromDifferentSender(prevMessage, message)
val nextSenderDiff = VkUtils.isPreviousMessageFromDifferentSender(message, nextMessage)
val fiveMinAgo = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message)
val change = (prevMessage?.date ?: 0) - message.date
Log.d(
"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 {
title?.isVisible = false
avatar?.isVisible = false
@@ -131,7 +137,6 @@ class MessagesPreparator constructor(
}
title.text = titleString
title.measure(0, 0)
}
}
@@ -164,13 +169,16 @@ class MessagesPreparator constructor(
attachmentContainer.removeAllViews()
} else {
attachmentContainer.isVisible = true
AttachmentInflater(
context = context,
container = attachmentContainer,
message = message,
groups = groups,
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 {
const val ONE_DAY_IN_SECONDS = 86400
fun removeTime(date: Date): Long {
return Calendar.getInstance().apply {
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:orientation="vertical"
android:padding="16dp"
android:visibility="gone"
app:layout_anchor="@+id/messagePanel"
app:layout_anchorGravity="center_vertical|top">
app:layout_anchorGravity="center_vertical|top"
tools:visibility="visible">
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/replyMessage"
@@ -249,10 +251,9 @@
android:id="@+id/dismissReply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ic_image_button_circle_background"
android:backgroundTint="@color/n1_50"
android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_round_close_20"
android:tint="?colorSecondary3" />
android:tint="@color/n1_800" />
</androidx.appcompat.widget.LinearLayoutCompat>
+14 -13
View File
@@ -70,19 +70,6 @@
android:textColor="@color/n1_800"
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>
</com.meloda.fast.widget.BoundedLinearLayout>
@@ -96,6 +83,20 @@
android:src="@color/a3_200" />
</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>
</layout>
+24 -33
View File
@@ -1,6 +1,5 @@
<?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.appcompat.widget.LinearLayoutCompat
@@ -35,44 +34,36 @@
android:id="@+id/bubble"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_gravity="end"
android:background="@drawable/ic_message_out_background"
android:clipChildren="true"
android:clipToPadding="true"
android:orientation="vertical">
<RelativeLayout
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text"
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>
<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>
</layout>
+12 -1
View File
@@ -21,6 +21,17 @@
android:id="@+id/messagesHistoryFragment"
android:name="com.meloda.fast.screens.messages.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>
+4
View File
@@ -117,4 +117,8 @@
<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_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>