forked from melod1n/fast-messenger
feat(messages): Implement "message read by" counter
This commit introduces the ability to see how many people have read an outgoing message in a group chat. A "views" count is now displayed in the message options dialog for relevant messages. - **API & Data Layer:** - Added `getMessageReadPeers` to `MessagesService` and `MessagesRepository` to fetch users who have read a specific message. - Introduced `MessagesGetReadPeersResponse` to handle the API response. - A new URL constant `GET_MESSAGE_READ_PEERS` was added. - **Domain Layer:** - A new `GetMessageReadPeersUseCase` is created to provide the view count to the ViewModel. - The use case is registered in the `DomainModule`. - **ViewModel:** - `MessagesHistoryViewModel` now includes `loadMessageReadPeers` to asynchronously fetch and return the view count for a message. - **UI (Compose):** - The `MessageOptionsDialog` now displays a "views" count for outgoing chat messages. - It uses a `LaunchedEffect` to call `loadMessageReadPeers` when the dialog is shown. - The `visibility` icon has been updated and its XML file renamed to `round_visibility_24px.xml` to follow a consistent naming convention. - **Refactoring & Minor Fixes:** - Simplified several `derivedStateOf` usages to direct calculations or property delegates in `MessageBubble.kt` and `MessagesList.kt` for minor performance improvements. - Renamed `IncomingMessageBubble.kt` and `OutgoingMessageBubble.kt` to `MessageBubbleIncoming.kt` and `MessageBubbleOutgoing.kt` respectively for consistency. - Removed an unnecessary log statement in `MessagesList.kt`.
This commit is contained in:
@@ -6,6 +6,7 @@ import dev.meloda.fast.model.api.domain.VkAttachment
|
|||||||
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
|
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
|
||||||
import dev.meloda.fast.model.api.domain.VkMessage
|
import dev.meloda.fast.model.api.domain.VkMessage
|
||||||
import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse
|
import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse
|
||||||
|
import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse
|
||||||
import dev.meloda.fast.model.api.responses.MessagesSendResponse
|
import dev.meloda.fast.model.api.responses.MessagesSendResponse
|
||||||
import dev.meloda.fast.network.RestApiErrorDomain
|
import dev.meloda.fast.network.RestApiErrorDomain
|
||||||
|
|
||||||
@@ -110,4 +111,9 @@ interface MessagesRepository {
|
|||||||
chatId: Long,
|
chatId: Long,
|
||||||
memberId: Long
|
memberId: Long
|
||||||
): ApiResult<Int, RestApiErrorDomain>
|
): ApiResult<Int, RestApiErrorDomain>
|
||||||
|
|
||||||
|
suspend fun getMessageReadPeers(
|
||||||
|
peerId: Long,
|
||||||
|
cmId: Long
|
||||||
|
): ApiResult<MessagesGetReadPeersResponse, RestApiErrorDomain>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import dev.meloda.fast.model.api.requests.MessagesRemoveChatUserRequest
|
|||||||
import dev.meloda.fast.model.api.requests.MessagesSendRequest
|
import dev.meloda.fast.model.api.requests.MessagesSendRequest
|
||||||
import dev.meloda.fast.model.api.requests.MessagesUnpinMessageRequest
|
import dev.meloda.fast.model.api.requests.MessagesUnpinMessageRequest
|
||||||
import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse
|
import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse
|
||||||
|
import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse
|
||||||
import dev.meloda.fast.model.api.responses.MessagesSendResponse
|
import dev.meloda.fast.model.api.responses.MessagesSendResponse
|
||||||
import dev.meloda.fast.network.RestApiErrorDomain
|
import dev.meloda.fast.network.RestApiErrorDomain
|
||||||
import dev.meloda.fast.network.mapApiDefault
|
import dev.meloda.fast.network.mapApiDefault
|
||||||
@@ -419,4 +420,18 @@ class MessagesRepositoryImpl(
|
|||||||
|
|
||||||
messagesService.removeChatUser(requestModel.map).mapApiDefault()
|
messagesService.removeChatUser(requestModel.map).mapApiDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getMessageReadPeers(
|
||||||
|
peerId: Long,
|
||||||
|
cmId: Long
|
||||||
|
): ApiResult<MessagesGetReadPeersResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||||
|
messagesService.getMessageReadPeers(
|
||||||
|
mapOf(
|
||||||
|
"peer_id" to peerId.toString(),
|
||||||
|
"cmid" to cmId.toString(),
|
||||||
|
"extended" to "1",
|
||||||
|
"fields" to VkConstants.USER_FIELDS
|
||||||
|
)
|
||||||
|
).mapApiDefault()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package dev.meloda.fast.domain
|
||||||
|
|
||||||
|
import dev.meloda.fast.data.State
|
||||||
|
import dev.meloda.fast.data.api.messages.MessagesRepository
|
||||||
|
import dev.meloda.fast.data.mapToState
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
class GetMessageReadPeersUseCase(
|
||||||
|
private val repository: MessagesRepository
|
||||||
|
) : BaseUseCase {
|
||||||
|
|
||||||
|
operator fun invoke(
|
||||||
|
peerId: Long,
|
||||||
|
cmId: Long
|
||||||
|
): Flow<State<Int>> = flowNewState {
|
||||||
|
repository.getMessageReadPeers(
|
||||||
|
peerId = peerId,
|
||||||
|
cmId = cmId
|
||||||
|
).mapToState(successMapper = { it.totalCount })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import dev.meloda.fast.domain.AccountUseCaseImpl
|
|||||||
import dev.meloda.fast.domain.GetCurrentAccountUseCase
|
import dev.meloda.fast.domain.GetCurrentAccountUseCase
|
||||||
import dev.meloda.fast.domain.GetLocalUserByIdUseCase
|
import dev.meloda.fast.domain.GetLocalUserByIdUseCase
|
||||||
import dev.meloda.fast.domain.GetLocalUsersByIdsUseCase
|
import dev.meloda.fast.domain.GetLocalUsersByIdsUseCase
|
||||||
|
import dev.meloda.fast.domain.GetMessageReadPeersUseCase
|
||||||
import dev.meloda.fast.domain.LoadConversationsByIdUseCase
|
import dev.meloda.fast.domain.LoadConversationsByIdUseCase
|
||||||
import dev.meloda.fast.domain.LoadUserByIdUseCase
|
import dev.meloda.fast.domain.LoadUserByIdUseCase
|
||||||
import dev.meloda.fast.domain.LoadUsersByIdsUseCase
|
import dev.meloda.fast.domain.LoadUsersByIdsUseCase
|
||||||
@@ -27,4 +28,6 @@ val domainModule = module {
|
|||||||
singleOf(::GetCurrentAccountUseCase)
|
singleOf(::GetCurrentAccountUseCase)
|
||||||
|
|
||||||
singleOf(::LoadConversationsByIdUseCase)
|
singleOf(::LoadConversationsByIdUseCase)
|
||||||
|
|
||||||
|
singleOf(::GetMessageReadPeersUseCase)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,3 +70,10 @@ data class MessagesMarkAsImportantResponse(
|
|||||||
@Json(name = "peer_id") val peerId: Long
|
@Json(name = "peer_id") val peerId: Long
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class MessagesGetReadPeersResponse(
|
||||||
|
@Json(name = "items") val items: List<Long>,
|
||||||
|
@Json(name = "total_count") val totalCount: Int,
|
||||||
|
@Json(name = "profiles") val profiles: List<VkUserData>?,
|
||||||
|
)
|
||||||
|
|||||||
+7
@@ -9,6 +9,7 @@ import dev.meloda.fast.model.api.responses.MessagesGetByIdResponse
|
|||||||
import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse
|
import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse
|
||||||
import dev.meloda.fast.model.api.responses.MessagesGetHistoryAttachmentsResponse
|
import dev.meloda.fast.model.api.responses.MessagesGetHistoryAttachmentsResponse
|
||||||
import dev.meloda.fast.model.api.responses.MessagesGetHistoryResponse
|
import dev.meloda.fast.model.api.responses.MessagesGetHistoryResponse
|
||||||
|
import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse
|
||||||
import dev.meloda.fast.model.api.responses.MessagesMarkAsImportantResponse
|
import dev.meloda.fast.model.api.responses.MessagesMarkAsImportantResponse
|
||||||
import dev.meloda.fast.model.api.responses.MessagesSendResponse
|
import dev.meloda.fast.model.api.responses.MessagesSendResponse
|
||||||
import dev.meloda.fast.network.ApiResponse
|
import dev.meloda.fast.network.ApiResponse
|
||||||
@@ -108,4 +109,10 @@ interface MessagesService {
|
|||||||
suspend fun removeChatUser(
|
suspend fun removeChatUser(
|
||||||
@FieldMap params: Map<String, String>
|
@FieldMap params: Map<String, String>
|
||||||
): ApiResult<ApiResponse<Int>, RestApiError>
|
): ApiResult<ApiResponse<Int>, RestApiError>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(MessagesUrls.GET_MESSAGE_READ_PEERS)
|
||||||
|
suspend fun getMessageReadPeers(
|
||||||
|
@FieldMap params: Map<String, String>
|
||||||
|
): ApiResult<ApiResponse<MessagesGetReadPeersResponse>, RestApiError>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,4 +22,5 @@ object MessagesUrls {
|
|||||||
const val REMOVE_CHAT_USER = "$URL/messages.removeChatUser"
|
const val REMOVE_CHAT_USER = "$URL/messages.removeChatUser"
|
||||||
const val GET_HISTORY_ATTACHMENTS = "$URL/messages.getHistoryAttachments"
|
const val GET_HISTORY_ATTACHMENTS = "$URL/messages.getHistoryAttachments"
|
||||||
const val CREATE_CHAT = "$URL/messages.createChat"
|
const val CREATE_CHAT = "$URL/messages.createChat"
|
||||||
|
const val GET_MESSAGE_READ_PEERS = "$URL/messages.getMessageReadPeers"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M12,4C7,4 2.73,7.11 1,11.5 2.73,15.89 7,19 12,19s9.27,-3.11 11,-7.5C21.27,7.11 17,4 12,4zM12,16.5c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,8.5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z" />
|
|
||||||
</vector>
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:pathData="M480,640Q555,640 607.5,587.5Q660,535 660,460Q660,385 607.5,332.5Q555,280 480,280Q405,280 352.5,332.5Q300,385 300,460Q300,535 352.5,587.5Q405,640 480,640ZM480,568Q435,568 403.5,536.5Q372,505 372,460Q372,415 403.5,383.5Q435,352 480,352Q525,352 556.5,383.5Q588,415 588,460Q588,505 556.5,536.5Q525,568 480,568ZM480,760Q334,760 214,678.5Q94,597 40,460Q94,323 214,241.5Q334,160 480,160Q626,160 746,241.5Q866,323 920,460Q866,597 746,678.5Q626,760 480,760ZM480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460ZM480,680Q593,680 687.5,620.5Q782,561 832,460Q782,359 687.5,299.5Q593,240 480,240Q367,240 272.5,299.5Q178,359 128,460Q178,561 272.5,620.5Q367,680 480,680Z" />
|
||||||
|
</vector>
|
||||||
@@ -296,7 +296,7 @@ fun LoginScreen(
|
|||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
val imagePainter = painterResource(
|
val imagePainter = painterResource(
|
||||||
id = if (screenState.passwordVisible) R.drawable.round_visibility_off_24
|
id = if (screenState.passwordVisible) R.drawable.round_visibility_off_24
|
||||||
else R.drawable.round_visibility_24
|
else R.drawable.round_visibility_24px
|
||||||
)
|
)
|
||||||
|
|
||||||
IconButton(onClick = onPasswordVisibilityButtonClicked) {
|
IconButton(onClick = onPasswordVisibilityButtonClicked) {
|
||||||
|
|||||||
+2
@@ -65,4 +65,6 @@ interface MessagesHistoryViewModel {
|
|||||||
fun onReplyCloseClicked()
|
fun onReplyCloseClicked()
|
||||||
|
|
||||||
fun onRequestReplyToMessage(cmId: Long)
|
fun onRequestReplyToMessage(cmId: Long)
|
||||||
|
|
||||||
|
suspend fun loadMessageReadPeers(peerId: Long, cmId: Long): Int
|
||||||
}
|
}
|
||||||
|
|||||||
+21
@@ -38,6 +38,7 @@ import dev.meloda.fast.data.processState
|
|||||||
import dev.meloda.fast.datastore.AppSettings
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
import dev.meloda.fast.datastore.UserSettings
|
import dev.meloda.fast.datastore.UserSettings
|
||||||
import dev.meloda.fast.domain.ConversationsUseCase
|
import dev.meloda.fast.domain.ConversationsUseCase
|
||||||
|
import dev.meloda.fast.domain.GetMessageReadPeersUseCase
|
||||||
import dev.meloda.fast.domain.LoadConversationsByIdUseCase
|
import dev.meloda.fast.domain.LoadConversationsByIdUseCase
|
||||||
import dev.meloda.fast.domain.LongPollUpdatesParser
|
import dev.meloda.fast.domain.LongPollUpdatesParser
|
||||||
import dev.meloda.fast.domain.MessagesUseCase
|
import dev.meloda.fast.domain.MessagesUseCase
|
||||||
@@ -70,6 +71,8 @@ import kotlinx.serialization.json.buildJsonObject
|
|||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@@ -80,6 +83,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
private val resourceProvider: ResourceProvider,
|
private val resourceProvider: ResourceProvider,
|
||||||
private val userSettings: UserSettings,
|
private val userSettings: UserSettings,
|
||||||
private val loadConversationsByIdUseCase: LoadConversationsByIdUseCase,
|
private val loadConversationsByIdUseCase: LoadConversationsByIdUseCase,
|
||||||
|
private val getMessageReadPeersUseCase: GetMessageReadPeersUseCase,
|
||||||
updatesParser: LongPollUpdatesParser,
|
updatesParser: LongPollUpdatesParser,
|
||||||
savedStateHandle: SavedStateHandle
|
savedStateHandle: SavedStateHandle
|
||||||
) : MessagesHistoryViewModel, ViewModel() {
|
) : MessagesHistoryViewModel, ViewModel() {
|
||||||
@@ -575,6 +579,23 @@ class MessagesHistoryViewModelImpl(
|
|||||||
replyToMessage(cmId)
|
replyToMessage(cmId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun loadMessageReadPeers(peerId: Long, cmId: Long): Int = suspendCoroutine {
|
||||||
|
viewModelScope.launch {
|
||||||
|
getMessageReadPeersUseCase
|
||||||
|
.invoke(peerId = peerId, cmId = cmId)
|
||||||
|
.listenValue(viewModelScope) { state ->
|
||||||
|
state.processState(
|
||||||
|
error = { error ->
|
||||||
|
it.resume(-1)
|
||||||
|
},
|
||||||
|
success = { count ->
|
||||||
|
it.resume(count)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
|
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
|
||||||
val message = event.message
|
val message = event.message
|
||||||
|
|
||||||
|
|||||||
+9
-15
@@ -83,9 +83,7 @@ fun MessageBubble(
|
|||||||
MaterialTheme.colorScheme.onPrimaryContainer
|
MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
val shouldShowBubble by remember(text) {
|
val shouldShowBubble = !text.isNullOrEmpty()
|
||||||
derivedStateOf { text != null }
|
|
||||||
}
|
|
||||||
|
|
||||||
var bubbleContainerWidth by remember {
|
var bubbleContainerWidth by remember {
|
||||||
mutableIntStateOf(0)
|
mutableIntStateOf(0)
|
||||||
@@ -95,25 +93,21 @@ fun MessageBubble(
|
|||||||
mutableIntStateOf(0)
|
mutableIntStateOf(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
val shouldFill by remember(bubbleContainerWidth, attachmentsContainerWidth) {
|
val shouldFill by derivedStateOf {
|
||||||
derivedStateOf {
|
attachmentsContainerWidth >= bubbleContainerWidth
|
||||||
attachmentsContainerWidth >= bubbleContainerWidth
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var containerWidth by remember {
|
var containerWidth by remember {
|
||||||
mutableIntStateOf(0)
|
mutableIntStateOf(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
val minDateContainerWidth by remember(isEdited, isOut, isPinned, isImportant) {
|
val minDateContainerWidth = remember(isEdited, isOut, isPinned, isImportant) {
|
||||||
derivedStateOf {
|
val mainPart = if (isEdited) 50 else 30
|
||||||
val mainPart = if (isEdited) 50 else 30
|
val readIndicatorPart = if (isOut) 14 else 0
|
||||||
val readIndicatorPart = if (isOut) 14 else 0
|
val pinnedIndicatorPart = if (isPinned) 14 else 0
|
||||||
val pinnedIndicatorPart = if (isPinned) 14 else 0
|
val importantIndicatorPart = if (isImportant) 14 else 0
|
||||||
val importantIndicatorPart = if (isImportant) 14 else 0
|
|
||||||
|
|
||||||
(mainPart + readIndicatorPart + pinnedIndicatorPart + importantIndicatorPart).dp
|
(mainPart + readIndicatorPart + pinnedIndicatorPart + importantIndicatorPart).dp
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val dateContainerWidth by animateDpAsState(
|
val dateContainerWidth by animateDpAsState(
|
||||||
|
|||||||
+74
-30
@@ -14,11 +14,13 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -38,6 +40,7 @@ import java.util.concurrent.TimeUnit
|
|||||||
fun HandleDialogs(
|
fun HandleDialogs(
|
||||||
screenState: MessagesHistoryScreenState,
|
screenState: MessagesHistoryScreenState,
|
||||||
dialog: MessageDialog?,
|
dialog: MessageDialog?,
|
||||||
|
messageReadPeersLoader: suspend (peerId: Long, cmId: Long) -> Int,
|
||||||
onConfirmed: (MessageDialog, Bundle) -> Unit = { _, _ -> },
|
onConfirmed: (MessageDialog, Bundle) -> Unit = { _, _ -> },
|
||||||
onDismissed: (MessageDialog) -> Unit = {},
|
onDismissed: (MessageDialog) -> Unit = {},
|
||||||
onItemPicked: (MessageDialog, Bundle) -> Unit = { _, _ -> }
|
onItemPicked: (MessageDialog, Bundle) -> Unit = { _, _ -> }
|
||||||
@@ -49,6 +52,7 @@ fun HandleDialogs(
|
|||||||
MessageOptionsDialog(
|
MessageOptionsDialog(
|
||||||
screenState = screenState,
|
screenState = screenState,
|
||||||
message = dialog.message,
|
message = dialog.message,
|
||||||
|
messageReadPeersLoader = messageReadPeersLoader,
|
||||||
onDismissed = { onDismissed(dialog) },
|
onDismissed = { onDismissed(dialog) },
|
||||||
onItemPicked = { bundle -> onItemPicked(dialog, bundle) }
|
onItemPicked = { bundle -> onItemPicked(dialog, bundle) }
|
||||||
)
|
)
|
||||||
@@ -102,9 +106,15 @@ fun HandleDialogs(
|
|||||||
fun MessageOptionsDialog(
|
fun MessageOptionsDialog(
|
||||||
screenState: MessagesHistoryScreenState,
|
screenState: MessagesHistoryScreenState,
|
||||||
message: VkMessage,
|
message: VkMessage,
|
||||||
|
messageReadPeersLoader: suspend (peerId: Long, cmId: Long) -> Int,
|
||||||
onDismissed: () -> Unit = {},
|
onDismissed: () -> Unit = {},
|
||||||
onItemPicked: (Bundle) -> Unit
|
onItemPicked: (Bundle) -> Unit
|
||||||
) {
|
) {
|
||||||
|
val primaryColor = MaterialTheme.colorScheme.primary
|
||||||
|
val errorColor = MaterialTheme.colorScheme.error
|
||||||
|
|
||||||
|
val showReadPeers = message.isOut && message.isPeerChat()
|
||||||
|
|
||||||
val options = mutableListOf<MessageOption>()
|
val options = mutableListOf<MessageOption>()
|
||||||
if (message.isFailed()) {
|
if (message.isFailed()) {
|
||||||
options += MessageOption.Retry
|
options += MessageOption.Retry
|
||||||
@@ -142,41 +152,47 @@ fun MessageOptionsDialog(
|
|||||||
|
|
||||||
options += MessageOption.Delete
|
options += MessageOption.Delete
|
||||||
|
|
||||||
val messageOptions = options.map { option ->
|
val messageOptions = remember(options) {
|
||||||
Triple(
|
options.map { option ->
|
||||||
stringResource(option.titleResId),
|
Triple(
|
||||||
painterResource(option.iconResId),
|
option.titleResId,
|
||||||
when {
|
option.iconResId,
|
||||||
option in listOf(
|
when {
|
||||||
MessageOption.Delete,
|
option in listOf(
|
||||||
MessageOption.MarkAsSpam
|
MessageOption.Delete,
|
||||||
) -> MaterialTheme.colorScheme.error
|
MessageOption.MarkAsSpam
|
||||||
|
) -> errorColor
|
||||||
|
|
||||||
else -> MaterialTheme.colorScheme.primary
|
else -> primaryColor
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialDialog(onDismissRequest = onDismissed) {
|
MaterialDialog(onDismissRequest = onDismissed) {
|
||||||
|
if (showReadPeers) {
|
||||||
|
var viewCount by remember {
|
||||||
|
mutableStateOf<Int?>(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewCount = messageReadPeersLoader.invoke(message.peerId, message.cmId)
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageOptionItem(
|
||||||
|
title = viewCount?.let { "$it views" } ?: "...",
|
||||||
|
iconResId = R.drawable.round_visibility_24px,
|
||||||
|
tintColor = primaryColor,
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
messageOptions
|
messageOptions
|
||||||
.forEachIndexed { index, (title, painter, tintColor) ->
|
.forEachIndexed { index, (titleResId, iconResId, tintColor) ->
|
||||||
DropdownMenuItem(
|
MessageOptionItem(
|
||||||
text = {
|
title = stringResource(titleResId),
|
||||||
Row {
|
iconResId = iconResId,
|
||||||
Text(text = title)
|
tintColor = tintColor,
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
Row {
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Icon(
|
|
||||||
painter = painter,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = tintColor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClick = {
|
onClick = {
|
||||||
onDismissed()
|
onDismissed()
|
||||||
val pickedOption = options[index]
|
val pickedOption = options[index]
|
||||||
@@ -193,6 +209,34 @@ fun MessageOptionsDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MessageOptionItem(
|
||||||
|
title: String,
|
||||||
|
iconResId: Int,
|
||||||
|
tintColor: Color,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Row {
|
||||||
|
Text(text = title)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Row {
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(iconResId),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = tintColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageDeleteDialog(
|
fun MessageDeleteDialog(
|
||||||
messages: List<VkMessage>,
|
messages: List<VkMessage>,
|
||||||
|
|||||||
+1
@@ -85,6 +85,7 @@ fun MessagesHistoryRoute(
|
|||||||
HandleDialogs(
|
HandleDialogs(
|
||||||
screenState = screenState,
|
screenState = screenState,
|
||||||
dialog = dialog,
|
dialog = dialog,
|
||||||
|
messageReadPeersLoader = viewModel::loadMessageReadPeers,
|
||||||
onConfirmed = viewModel::onDialogConfirmed,
|
onConfirmed = viewModel::onDialogConfirmed,
|
||||||
onDismissed = viewModel::onDialogDismissed,
|
onDismissed = viewModel::onDialogDismissed,
|
||||||
onItemPicked = viewModel::onDialogItemPicked
|
onItemPicked = viewModel::onDialogItemPicked
|
||||||
|
|||||||
+35
-40
@@ -78,22 +78,21 @@ fun MessagesList(
|
|||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val onAttachmentClick by rememberUpdatedState(
|
val onAttachmentClick by rememberUpdatedState { message: UiItem.Message, attachment: VkAttachment ->
|
||||||
{ message: UiItem.Message, attachment: VkAttachment ->
|
if (isSelectedAtLeastOne) {
|
||||||
if (isSelectedAtLeastOne) {
|
onMessageClicked(message.id)
|
||||||
onMessageClicked(message.id)
|
} else {
|
||||||
} else {
|
when (attachment) {
|
||||||
when (attachment) {
|
is VkPhotoDomain -> {
|
||||||
is VkPhotoDomain -> {
|
val photos = message.attachments
|
||||||
val photos = message.attachments
|
.orEmpty()
|
||||||
.orEmpty()
|
.filterIsInstance<VkPhotoDomain>()
|
||||||
.filterIsInstance<VkPhotoDomain>()
|
.mapNotNull { photo -> photo.getMaxSize()?.url }
|
||||||
.mapNotNull { photo -> photo.getMaxSize()?.url }
|
|
||||||
|
|
||||||
onPhotoClicked(
|
onPhotoClicked(
|
||||||
photos,
|
photos,
|
||||||
photos.indexOfFirst { it == attachment.getMaxSize()?.url }
|
photos.indexOfFirst { it == attachment.getMaxSize()?.url }
|
||||||
)
|
)
|
||||||
|
|
||||||
// val maxSize = attachment.getMaxSize()
|
// val maxSize = attachment.getMaxSize()
|
||||||
// maxSize?.let {
|
// maxSize?.let {
|
||||||
@@ -101,39 +100,36 @@ fun MessagesList(
|
|||||||
// Intent(Intent.ACTION_VIEW, maxSize.url.toUri())
|
// Intent(Intent.ACTION_VIEW, maxSize.url.toUri())
|
||||||
// )
|
// )
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
is VkFileDomain -> {
|
is VkFileDomain -> {
|
||||||
context.startActivity(
|
context.startActivity(
|
||||||
Intent(Intent.ACTION_VIEW, attachment.url.toUri())
|
Intent(Intent.ACTION_VIEW, attachment.url.toUri())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is VkLinkDomain -> {
|
is VkLinkDomain -> {
|
||||||
context.startActivity(
|
context.startActivity(
|
||||||
Intent(Intent.ACTION_VIEW, attachment.url.toUri())
|
Intent(Intent.ACTION_VIEW, attachment.url.toUri())
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
val onAttachmentLongClick by rememberUpdatedState(
|
val onAttachmentLongClick by rememberUpdatedState { message: UiItem.Message, attachment: VkAttachment ->
|
||||||
{ message: UiItem.Message, attachment: VkAttachment ->
|
if (isSelectedAtLeastOne) {
|
||||||
if (isSelectedAtLeastOne) {
|
onMessageLongClicked(message.id)
|
||||||
onMessageLongClicked(message.id)
|
uiMessages
|
||||||
uiMessages
|
} else {
|
||||||
} else {
|
when (attachment) {
|
||||||
when (attachment) {
|
is VkPhotoDomain -> {
|
||||||
is VkPhotoDomain -> {
|
val maxSize = attachment.getMaxSize()
|
||||||
val maxSize = attachment.getMaxSize()
|
Log.d("MessagesList", "onPhotoLongClicked. Max size: ${maxSize?.url}")
|
||||||
Log.d("MessagesList", "onPhotoLongClicked. Max size: ${maxSize?.url}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -207,7 +203,6 @@ fun MessagesList(
|
|||||||
if (offsetDistinct == -100f && AppSettings.General.enableHaptic) {
|
if (offsetDistinct == -100f && AppSettings.General.enableHaptic) {
|
||||||
view.performHapticFeedback(HapticFeedbackConstantsCompat.CONTEXT_CLICK)
|
view.performHapticFeedback(HapticFeedbackConstantsCompat.CONTEXT_CLICK)
|
||||||
}
|
}
|
||||||
Log.d("MessagesList", "offsetDistinct: $offsetDistinct")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
|
|||||||
+3
-3
@@ -28,14 +28,14 @@ import androidx.compose.ui.unit.dp
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Reply(
|
fun Reply(
|
||||||
modifier: Modifier = Modifier,
|
onClick: () -> Unit,
|
||||||
bottomPadding: Dp,
|
bottomPadding: Dp,
|
||||||
shape: Shape,
|
shape: Shape,
|
||||||
onClick: () -> Unit,
|
|
||||||
backgroundColor: Color,
|
backgroundColor: Color,
|
||||||
innerBackgroundColor: Color,
|
innerBackgroundColor: Color,
|
||||||
title: String,
|
title: String,
|
||||||
summary: String?
|
summary: String?,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|||||||
Reference in New Issue
Block a user