forked from melod1n/fast-messenger
Release 0.2.0 (#150)
Release Notes * Bumped haze, agp, and guava dependencies * Implemented ordering functionality for friends list * Added scroll to top feature in friends and conversations screens * Improved messages handling * Fixed coloring issues * Cache improvements * Implemented logout functionality * Implemented new authorization flow (no auto-token re-request) * Added support for sticker pack preview attachments * Bump LongPoll to version 19 * Markdown support for messages bubbles * Adjust app name font size based on screen width --------- Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
+393
-425
File diff suppressed because it is too large
Load Diff
+25
-2
@@ -3,12 +3,35 @@ package dev.meloda.fast.conversations.di
|
||||
import dev.meloda.fast.conversations.ConversationsViewModelImpl
|
||||
import dev.meloda.fast.domain.ConversationsUseCase
|
||||
import dev.meloda.fast.domain.ConversationsUseCaseImpl
|
||||
import dev.meloda.fast.model.ConversationsFilter
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.core.module.dsl.viewModel
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.core.scope.Scope
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
val conversationsModule = module {
|
||||
viewModel(named(ConversationsFilter.ALL)) {
|
||||
createConversationsViewModel(ConversationsFilter.ALL)
|
||||
}
|
||||
viewModel(named(ConversationsFilter.ARCHIVE)) {
|
||||
createConversationsViewModel(ConversationsFilter.ARCHIVE)
|
||||
}
|
||||
|
||||
singleOf(::ConversationsUseCaseImpl) bind ConversationsUseCase::class
|
||||
viewModelOf(::ConversationsViewModelImpl)
|
||||
}
|
||||
|
||||
private fun Scope.createConversationsViewModel(filter: ConversationsFilter): ConversationsViewModelImpl {
|
||||
return ConversationsViewModelImpl(
|
||||
filter = filter,
|
||||
updatesParser = get(),
|
||||
conversationsUseCase = get(),
|
||||
messagesUseCase = get(),
|
||||
resources = get(),
|
||||
userSettings = get(),
|
||||
imageLoader = get(),
|
||||
applicationContext = get(),
|
||||
loadConversationsByIdUseCase = get()
|
||||
)
|
||||
}
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package dev.meloda.fast.conversations.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
sealed class ConversationDialog {
|
||||
data class ConversationPin(val conversationId: Long) : ConversationDialog()
|
||||
data class ConversationUnpin(val conversationId: Long) : ConversationDialog()
|
||||
data class ConversationDelete(val conversationId: Long) : ConversationDialog()
|
||||
data class ConversationArchive(val conversationId: Long) : ConversationDialog()
|
||||
data class ConversationUnarchive(val conversationId: Long) : ConversationDialog()
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package dev.meloda.fast.conversations.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
sealed class ConversationNavigation {
|
||||
|
||||
data class MessagesHistory(val peerId: Long) : ConversationNavigation()
|
||||
|
||||
data object CreateChat : ConversationNavigation()
|
||||
}
|
||||
+3
-6
@@ -1,31 +1,28 @@
|
||||
package dev.meloda.fast.conversations.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import dev.meloda.fast.ui.model.api.ConversationsShowOptions
|
||||
import dev.meloda.fast.ui.model.api.UiConversation
|
||||
|
||||
@Immutable
|
||||
data class ConversationsScreenState(
|
||||
val showOptions: ConversationsShowOptions,
|
||||
val conversations: List<UiConversation>,
|
||||
val isLoading: Boolean,
|
||||
val isPaginating: Boolean,
|
||||
val isPaginationExhausted: Boolean,
|
||||
val profileImageUrl: String?,
|
||||
val scrollIndex: Int,
|
||||
val scrollOffset: Int
|
||||
val scrollOffset: Int,
|
||||
val isArchive: Boolean
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY: ConversationsScreenState = ConversationsScreenState(
|
||||
showOptions = ConversationsShowOptions.EMPTY,
|
||||
conversations = emptyList(),
|
||||
isLoading = true,
|
||||
isPaginating = false,
|
||||
isPaginationExhausted = false,
|
||||
profileImageUrl = null,
|
||||
scrollIndex = 0,
|
||||
scrollOffset = 0,
|
||||
isArchive = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package dev.meloda.fast.conversations.model
|
||||
|
||||
import dev.meloda.fast.model.InteractionType
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
data class InteractionJob(
|
||||
val interactionType: InteractionType,
|
||||
val timerJob: Job
|
||||
)
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package dev.meloda.fast.conversations.model
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
class NewInteractionException : CancellationException()
|
||||
+58
-20
@@ -1,35 +1,73 @@
|
||||
package dev.meloda.fast.conversations.navigation
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import dev.meloda.fast.conversations.ConversationsViewModel
|
||||
import androidx.navigation.navigation
|
||||
import dev.meloda.fast.conversations.ConversationsViewModelImpl
|
||||
import dev.meloda.fast.conversations.presentation.ConversationsRoute
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.extensions.sharedViewModel
|
||||
import dev.meloda.fast.model.ConversationsFilter
|
||||
import dev.meloda.fast.ui.theme.LocalNavController
|
||||
import dev.meloda.fast.ui.theme.getOrThrow
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.core.qualifier.named
|
||||
|
||||
@Serializable
|
||||
object ConversationsGraph
|
||||
|
||||
@Serializable
|
||||
object Conversations
|
||||
|
||||
fun NavGraphBuilder.conversationsScreen(
|
||||
onError: (BaseError) -> Unit,
|
||||
onConversationItemClicked: (id: Int) -> Unit,
|
||||
onPhotoClicked: (url: String) -> Unit,
|
||||
onCreateChatClicked: () -> Unit,
|
||||
navController: NavController,
|
||||
) {
|
||||
composable<Conversations> {
|
||||
val viewModel: ConversationsViewModel =
|
||||
it.sharedViewModel<ConversationsViewModelImpl>(navController = navController)
|
||||
@Serializable
|
||||
object Archive
|
||||
|
||||
ConversationsRoute(
|
||||
onError = onError,
|
||||
onConversationItemClicked = onConversationItemClicked,
|
||||
onConversationPhotoClicked = onPhotoClicked,
|
||||
onCreateChatButtonClicked = onCreateChatClicked,
|
||||
viewModel = viewModel
|
||||
)
|
||||
fun NavGraphBuilder.conversationsGraph(
|
||||
onError: (BaseError) -> Unit,
|
||||
onNavigateToMessagesHistory: (id: Long) -> Unit,
|
||||
onNavigateToCreateChat: () -> Unit,
|
||||
onScrolledToTop: () -> Unit
|
||||
) {
|
||||
navigation<ConversationsGraph>(
|
||||
startDestination = Conversations
|
||||
) {
|
||||
composable<Conversations> {
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.getOrThrow()
|
||||
|
||||
val viewModel: ConversationsViewModelImpl = koinViewModel(
|
||||
qualifier = named(ConversationsFilter.ALL),
|
||||
viewModelStoreOwner = context as AppCompatActivity
|
||||
)
|
||||
|
||||
ConversationsRoute(
|
||||
viewModel = viewModel,
|
||||
onError = onError,
|
||||
onNavigateToMessagesHistory = onNavigateToMessagesHistory,
|
||||
onNavigateToCreateChat = onNavigateToCreateChat,
|
||||
onNavigateToArchive = { navController.navigate(Archive) },
|
||||
onScrolledToTop = onScrolledToTop
|
||||
)
|
||||
}
|
||||
composable<Archive> {
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.getOrThrow()
|
||||
|
||||
val viewModel: ConversationsViewModelImpl = koinViewModel(
|
||||
qualifier = named(ConversationsFilter.ARCHIVE),
|
||||
viewModelStoreOwner = context as AppCompatActivity
|
||||
)
|
||||
|
||||
ConversationsRoute(
|
||||
viewModel = viewModel,
|
||||
onBack = navController::navigateUp,
|
||||
onError = onError,
|
||||
onNavigateToMessagesHistory = onNavigateToMessagesHistory,
|
||||
onScrolledToTop = onScrolledToTop
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package dev.meloda.fast.conversations.presentation
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.os.bundleOf
|
||||
import dev.meloda.fast.conversations.model.ConversationDialog
|
||||
import dev.meloda.fast.conversations.model.ConversationsScreenState
|
||||
import dev.meloda.fast.ui.components.MaterialDialog
|
||||
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
@Composable
|
||||
fun HandleDialogs(
|
||||
screenState: ConversationsScreenState,
|
||||
dialog: ConversationDialog?,
|
||||
onConfirmed: (ConversationDialog, Bundle) -> Unit = { _, _ -> },
|
||||
onDismissed: (ConversationDialog) -> Unit = {},
|
||||
onItemPicked: (ConversationDialog, Bundle) -> Unit = { _, _ -> }
|
||||
) {
|
||||
when (dialog) {
|
||||
null -> Unit
|
||||
|
||||
is ConversationDialog.ConversationArchive -> {
|
||||
MaterialDialog(
|
||||
onDismissRequest = { onDismissed(dialog) },
|
||||
title = stringResource(id = UiR.string.confirm_archive_conversation),
|
||||
confirmAction = { onConfirmed(dialog, bundleOf()) },
|
||||
confirmText = stringResource(id = UiR.string.action_archive),
|
||||
cancelText = stringResource(id = UiR.string.cancel)
|
||||
)
|
||||
}
|
||||
|
||||
is ConversationDialog.ConversationUnarchive -> {
|
||||
MaterialDialog(
|
||||
onDismissRequest = { onDismissed(dialog) },
|
||||
title = stringResource(id = UiR.string.confirm_unarchive_conversation),
|
||||
confirmAction = { onConfirmed(dialog, bundleOf()) },
|
||||
confirmText = stringResource(id = UiR.string.action_unarchive),
|
||||
cancelText = stringResource(id = UiR.string.cancel)
|
||||
)
|
||||
}
|
||||
|
||||
is ConversationDialog.ConversationDelete -> {
|
||||
MaterialDialog(
|
||||
onDismissRequest = { onDismissed(dialog) },
|
||||
title = stringResource(id = UiR.string.confirm_delete_conversation),
|
||||
confirmAction = { onConfirmed(dialog, bundleOf()) },
|
||||
confirmText = stringResource(id = UiR.string.action_delete),
|
||||
cancelText = stringResource(id = UiR.string.cancel)
|
||||
)
|
||||
}
|
||||
|
||||
is ConversationDialog.ConversationPin -> {
|
||||
MaterialDialog(
|
||||
onDismissRequest = { onDismissed(dialog) },
|
||||
title = stringResource(id = UiR.string.confirm_pin_conversation),
|
||||
confirmAction = { onConfirmed(dialog, bundleOf()) },
|
||||
confirmText = stringResource(id = UiR.string.action_pin),
|
||||
cancelText = stringResource(id = UiR.string.cancel)
|
||||
)
|
||||
}
|
||||
|
||||
is ConversationDialog.ConversationUnpin -> {
|
||||
MaterialDialog(
|
||||
onDismissRequest = { onDismissed(dialog) },
|
||||
title = stringResource(id = UiR.string.confirm_unpin_conversation),
|
||||
confirmAction = { onConfirmed(dialog, bundleOf()) },
|
||||
confirmText = stringResource(id = UiR.string.action_unpin),
|
||||
cancelText = stringResource(id = UiR.string.cancel)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-25
@@ -6,10 +6,7 @@ import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -21,7 +18,8 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ElevatedAssistChip
|
||||
@@ -40,7 +38,6 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
@@ -63,16 +60,14 @@ val BirthdayColor = Color(0xffb00b69)
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ConversationItem(
|
||||
onItemClick: (Int) -> Unit,
|
||||
onItemClick: (UiConversation) -> Unit,
|
||||
onItemLongClick: (conversation: UiConversation) -> Unit,
|
||||
onOptionClicked: (UiConversation, ConversationOption) -> Unit,
|
||||
maxLines: Int,
|
||||
isUserAccount: Boolean,
|
||||
conversation: UiConversation,
|
||||
modifier: Modifier = Modifier,
|
||||
onPhotoClicked: (url: String) -> Unit
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
|
||||
val bottomStartCornerRadius by animateDpAsState(
|
||||
@@ -84,7 +79,7 @@ fun ConversationItem(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onClick = { onItemClick(conversation.id) },
|
||||
onClick = { onItemClick(conversation) },
|
||||
onLongClick = {
|
||||
onItemLongClick(conversation)
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
@@ -154,12 +149,7 @@ fun ConversationItem(
|
||||
contentDescription = "Avatar",
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(CircleShape)
|
||||
.clickable {
|
||||
if (avatarImage is String) {
|
||||
onPhotoClicked(avatarImage)
|
||||
}
|
||||
},
|
||||
.clip(CircleShape),
|
||||
placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut)
|
||||
)
|
||||
}
|
||||
@@ -250,7 +240,7 @@ fun ConversationItem(
|
||||
text = conversation.title,
|
||||
minLines = 1,
|
||||
maxLines = maxLines,
|
||||
style = MaterialTheme.typography.headlineSmall.copy(fontSize = 20.sp)
|
||||
style = MaterialTheme.typography.headlineSmall.copy(fontSize = 20.sp),
|
||||
)
|
||||
|
||||
Row {
|
||||
@@ -338,9 +328,13 @@ fun ConversationItem(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.defaultMinSize(minWidth = 20.dp, minHeight = 20.dp)
|
||||
.defaultMinSize(
|
||||
minWidth = 20.dp,
|
||||
minHeight = 20.dp
|
||||
)
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(horizontal = if (count.length > 1) 2.dp else 0.dp)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
@@ -361,18 +355,19 @@ fun ConversationItem(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(60.dp)
|
||||
.padding(start = 8.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Row(
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.padding(horizontal = 10.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 10.dp)
|
||||
) {
|
||||
conversation.options.forEach { option ->
|
||||
items(conversation.options.toList()) { option ->
|
||||
ElevatedAssistChip(
|
||||
onClick = { onOptionClicked(conversation, option) },
|
||||
leadingIcon = {
|
||||
@@ -388,6 +383,7 @@ fun ConversationItem(
|
||||
Text(text = option.title.getString().orEmpty())
|
||||
}
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,5 +398,3 @@ fun ConversationItem(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+23
-10
@@ -27,25 +27,26 @@ import dev.meloda.fast.data.UserConfig
|
||||
import dev.meloda.fast.ui.model.api.ConversationOption
|
||||
import dev.meloda.fast.ui.model.api.UiConversation
|
||||
import dev.meloda.fast.ui.theme.LocalBottomPadding
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ConversationsList(
|
||||
onConversationsClick: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
conversations: ImmutableList<UiConversation>,
|
||||
onConversationsClick: (UiConversation) -> Unit,
|
||||
onConversationsLongClick: (UiConversation) -> Unit,
|
||||
screenState: ConversationsScreenState,
|
||||
state: LazyListState,
|
||||
maxLines: Int,
|
||||
modifier: Modifier,
|
||||
onOptionClicked: (UiConversation, ConversationOption) -> Unit,
|
||||
padding: PaddingValues,
|
||||
onPhotoClicked: (url: String) -> Unit
|
||||
padding: PaddingValues
|
||||
) {
|
||||
val theme = LocalThemeConfig.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val bottomPadding = LocalBottomPadding.current
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
state = state
|
||||
@@ -55,7 +56,7 @@ fun ConversationsList(
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
items(
|
||||
items = screenState.conversations,
|
||||
items = conversations.values,
|
||||
key = UiConversation::id,
|
||||
) { conversation ->
|
||||
val isUserAccount by remember(conversation) {
|
||||
@@ -71,8 +72,12 @@ fun ConversationsList(
|
||||
maxLines = maxLines,
|
||||
isUserAccount = isUserAccount,
|
||||
conversation = conversation,
|
||||
modifier = Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
||||
onPhotoClicked = onPhotoClicked
|
||||
modifier =
|
||||
if (theme.enableAnimations) Modifier.animateItem(
|
||||
fadeInSpec = null,
|
||||
fadeOutSpec = null
|
||||
)
|
||||
else Modifier
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
@@ -82,7 +87,14 @@ fun ConversationsList(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
||||
.then(
|
||||
if (theme.enableAnimations)
|
||||
Modifier.animateItem(
|
||||
fadeInSpec = null,
|
||||
fadeOutSpec = null
|
||||
)
|
||||
else Modifier
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (screenState.isPaginating) {
|
||||
@@ -107,6 +119,7 @@ fun ConversationsList(
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(LocalBottomPadding.current))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package dev.meloda.fast.conversations.presentation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.meloda.fast.conversations.ConversationsViewModel
|
||||
import dev.meloda.fast.conversations.model.ConversationNavigation
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun ConversationsRoute(
|
||||
viewModel: ConversationsViewModel,
|
||||
onBack: (() -> Unit)? = null,
|
||||
onError: (BaseError) -> Unit,
|
||||
onNavigateToMessagesHistory: (conversationId: Long) -> Unit,
|
||||
onNavigateToCreateChat: (() -> Unit)? = null,
|
||||
onNavigateToArchive: (() -> Unit)? = null,
|
||||
onScrolledToTop: () -> Unit,
|
||||
) {
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val navigationEvent by viewModel.navigation.collectAsStateWithLifecycle()
|
||||
val conversations by viewModel.uiConversations.collectAsStateWithLifecycle()
|
||||
val dialog by viewModel.dialog.collectAsStateWithLifecycle()
|
||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(navigationEvent) {
|
||||
val shouldBeConsumed: Boolean = when (val navigation = navigationEvent) {
|
||||
null -> false
|
||||
|
||||
is ConversationNavigation.CreateChat -> {
|
||||
onNavigateToCreateChat?.invoke()
|
||||
true
|
||||
}
|
||||
|
||||
is ConversationNavigation.MessagesHistory -> {
|
||||
onNavigateToMessagesHistory(navigation.peerId)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldBeConsumed) viewModel.onNavigationConsumed()
|
||||
}
|
||||
|
||||
ConversationsScreen(
|
||||
onBack = { onBack?.invoke() },
|
||||
screenState = screenState,
|
||||
conversations = conversations.toImmutableList(),
|
||||
baseError = baseError,
|
||||
canPaginate = canPaginate,
|
||||
onConversationItemClicked = viewModel::onConversationItemClick,
|
||||
onConversationItemLongClicked = viewModel::onConversationItemLongClick,
|
||||
onOptionClicked = viewModel::onOptionClicked,
|
||||
onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
|
||||
onRefreshDropdownItemClicked = viewModel::onRefresh,
|
||||
onRefresh = viewModel::onRefresh,
|
||||
onCreateChatButtonClicked = viewModel::onCreateChatButtonClicked,
|
||||
onArchiveActionClicked = { onNavigateToArchive?.invoke() },
|
||||
setScrollIndex = viewModel::setScrollIndex,
|
||||
setScrollOffset = viewModel::setScrollOffset,
|
||||
onConsumeReselection = onScrolledToTop,
|
||||
onErrorViewButtonClicked = {
|
||||
if (baseError in listOf(BaseError.AccountBlocked, BaseError.SessionExpired)) {
|
||||
onError(requireNotNull(baseError))
|
||||
} else {
|
||||
viewModel.onErrorButtonClicked()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
HandleDialogs(
|
||||
screenState = screenState,
|
||||
dialog = dialog,
|
||||
onConfirmed = viewModel::onDialogConfirmed,
|
||||
onDismissed = viewModel::onDialogDismissed,
|
||||
onItemPicked = viewModel::onDialogItemPicked
|
||||
)
|
||||
}
|
||||
+82
-128
@@ -3,11 +3,8 @@ package dev.meloda.fast.conversations.presentation
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.animateIntAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideIn
|
||||
import androidx.compose.animation.slideOut
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
@@ -17,11 +14,13 @@ import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material.icons.rounded.MoreVert
|
||||
import androidx.compose.material.icons.rounded.Refresh
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
@@ -51,7 +50,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -59,66 +57,30 @@ import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import dev.meloda.fast.conversations.ConversationsViewModel
|
||||
import dev.meloda.fast.conversations.model.ConversationsScreenState
|
||||
import dev.meloda.fast.conversations.navigation.Conversations
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.components.ErrorView
|
||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
||||
import dev.meloda.fast.ui.components.MaterialDialog
|
||||
import dev.meloda.fast.ui.components.NoItemsView
|
||||
import dev.meloda.fast.ui.components.VkErrorView
|
||||
import dev.meloda.fast.ui.model.api.ConversationOption
|
||||
import dev.meloda.fast.ui.model.api.UiConversation
|
||||
import dev.meloda.fast.ui.theme.LocalBottomPadding
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
import dev.meloda.fast.ui.theme.LocalReselectedTab
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
import dev.meloda.fast.ui.util.emptyImmutableList
|
||||
import dev.meloda.fast.ui.util.isScrollingUp
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
@Composable
|
||||
fun ConversationsRoute(
|
||||
onError: (BaseError) -> Unit,
|
||||
onConversationItemClicked: (conversationId: Int) -> Unit,
|
||||
onConversationPhotoClicked: (url: String) -> Unit,
|
||||
onCreateChatButtonClicked: () -> Unit,
|
||||
viewModel: ConversationsViewModel
|
||||
) {
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
|
||||
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
|
||||
|
||||
ConversationsScreen(
|
||||
screenState = screenState,
|
||||
baseError = baseError,
|
||||
canPaginate = canPaginate,
|
||||
onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) },
|
||||
onConversationItemClicked = { id ->
|
||||
onConversationItemClicked(id)
|
||||
viewModel.onConversationItemClick()
|
||||
},
|
||||
onConversationItemLongClicked = viewModel::onConversationItemLongClick,
|
||||
onOptionClicked = viewModel::onOptionClicked,
|
||||
onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
|
||||
onRefreshDropdownItemClicked = viewModel::onRefresh,
|
||||
onRefresh = viewModel::onRefresh,
|
||||
onConversationPhotoClicked = onConversationPhotoClicked,
|
||||
onCreateChatButtonClicked = onCreateChatButtonClicked,
|
||||
setScrollIndex = viewModel::setScrollIndex,
|
||||
setScrollOffset = viewModel::setScrollOffset
|
||||
)
|
||||
|
||||
HandleDialogs(
|
||||
screenState = screenState,
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(
|
||||
ExperimentalMaterial3Api::class,
|
||||
ExperimentalHazeMaterialsApi::class,
|
||||
@@ -126,21 +88,23 @@ fun ConversationsRoute(
|
||||
@Composable
|
||||
fun ConversationsScreen(
|
||||
screenState: ConversationsScreenState = ConversationsScreenState.EMPTY,
|
||||
conversations: ImmutableList<UiConversation> = emptyImmutableList(),
|
||||
baseError: BaseError? = null,
|
||||
canPaginate: Boolean = false,
|
||||
onSessionExpiredLogOutButtonClicked: () -> Unit = {},
|
||||
onConversationItemClicked: (conversationId: Int) -> Unit = {},
|
||||
onBack: () -> Unit = {},
|
||||
onConversationItemClicked: (conversation: UiConversation) -> Unit = {},
|
||||
onConversationItemLongClicked: (conversation: UiConversation) -> Unit = {},
|
||||
onOptionClicked: (UiConversation, ConversationOption) -> Unit = { _, _ -> },
|
||||
onPaginationConditionsMet: () -> Unit = {},
|
||||
onRefreshDropdownItemClicked: () -> Unit = {},
|
||||
onRefresh: () -> Unit = {},
|
||||
onConversationPhotoClicked: (url: String) -> Unit = {},
|
||||
onCreateChatButtonClicked: () -> Unit = {},
|
||||
onArchiveActionClicked: () -> Unit = {},
|
||||
setScrollIndex: (Int) -> Unit = {},
|
||||
setScrollOffset: (Int) -> Unit = {}
|
||||
setScrollOffset: (Int) -> Unit = {},
|
||||
onConsumeReselection: () -> Unit = {},
|
||||
onErrorViewButtonClicked: () -> Unit = {}
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
|
||||
val maxLines by remember(currentTheme) {
|
||||
@@ -152,6 +116,21 @@ fun ConversationsScreen(
|
||||
initialFirstVisibleItemScrollOffset = screenState.scrollOffset
|
||||
)
|
||||
|
||||
val currentTabReselected = LocalReselectedTab.current[Conversations] ?: false
|
||||
LaunchedEffect(currentTabReselected) {
|
||||
if (currentTabReselected) {
|
||||
if (screenState.isArchive) {
|
||||
onBack.invoke()
|
||||
} else {
|
||||
if (listState.firstVisibleItemIndex > 14) {
|
||||
listState.scrollToItem(14)
|
||||
}
|
||||
listState.animateScrollToItem(0)
|
||||
onConsumeReselection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow { listState.firstVisibleItemIndex }
|
||||
.debounce(500L)
|
||||
@@ -209,22 +188,40 @@ fun ConversationsScreen(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = if (screenState.isLoading) UiR.string.title_loading
|
||||
else UiR.string.title_conversations
|
||||
id = when {
|
||||
screenState.isLoading -> UiR.string.title_loading
|
||||
screenState.isArchive -> UiR.string.title_archive
|
||||
else -> UiR.string.title_conversations
|
||||
}
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = {
|
||||
dropDownMenuExpanded = true
|
||||
navigationIcon = {
|
||||
if (screenState.isArchive) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
) {
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (!screenState.isArchive) {
|
||||
IconButton(onClick = onArchiveActionClicked) {
|
||||
Icon(
|
||||
painter = painterResource(UiR.drawable.outline_archive_24),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = { dropDownMenuExpanded = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
imageVector = Icons.Rounded.MoreVert,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
@@ -270,7 +267,7 @@ fun ConversationsScreen(
|
||||
)
|
||||
|
||||
val showHorizontalProgressBar by remember(screenState) {
|
||||
derivedStateOf { screenState.isLoading && screenState.conversations.isNotEmpty() }
|
||||
derivedStateOf { screenState.isLoading && conversations.isNotEmpty() }
|
||||
}
|
||||
AnimatedVisibility(showHorizontalProgressBar) {
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
@@ -281,46 +278,38 @@ fun ConversationsScreen(
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
Column {
|
||||
AnimatedVisibility(
|
||||
visible = listState.isScrollingUp(),
|
||||
enter = slideIn { IntOffset(0, 600) } + fadeIn(tween(200)),
|
||||
exit = slideOut { IntOffset(0, 600) } + fadeOut(tween(200))
|
||||
) {
|
||||
FloatingActionButton(onClick = onCreateChatButtonClicked) {
|
||||
if (!screenState.isArchive) {
|
||||
val offsetY by animateIntAsState(
|
||||
targetValue = if (listState.isScrollingUp()) 0 else 600
|
||||
)
|
||||
|
||||
Column {
|
||||
FloatingActionButton(
|
||||
onClick = onCreateChatButtonClicked,
|
||||
modifier = Modifier.offset {
|
||||
IntOffset(0, offsetY)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = UiR.drawable.ic_baseline_create_24),
|
||||
painter = painterResource(id = UiR.drawable.round_create_24),
|
||||
contentDescription = "Add chat button"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(LocalBottomPadding.current))
|
||||
Spacer(modifier = Modifier.height(LocalBottomPadding.current))
|
||||
}
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
when {
|
||||
baseError != null -> {
|
||||
when (baseError) {
|
||||
is BaseError.SessionExpired -> {
|
||||
ErrorView(
|
||||
text = stringResource(UiR.string.session_expired),
|
||||
buttonText = stringResource(UiR.string.action_log_out),
|
||||
onButtonClick = onSessionExpiredLogOutButtonClicked
|
||||
)
|
||||
}
|
||||
|
||||
is BaseError.SimpleError -> {
|
||||
ErrorView(
|
||||
text = baseError.message,
|
||||
buttonText = stringResource(UiR.string.try_again),
|
||||
onButtonClick = onRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
VkErrorView(
|
||||
baseError = baseError,
|
||||
onButtonClick = onErrorViewButtonClicked
|
||||
)
|
||||
}
|
||||
|
||||
screenState.isLoading && screenState.conversations.isEmpty() -> FullScreenLoader()
|
||||
screenState.isLoading && conversations.isEmpty() -> FullScreenLoader()
|
||||
|
||||
else -> {
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
@@ -345,6 +334,7 @@ fun ConversationsScreen(
|
||||
}
|
||||
) {
|
||||
ConversationsList(
|
||||
conversations = conversations,
|
||||
onConversationsClick = onConversationItemClicked,
|
||||
onConversationsLongClick = onConversationItemLongClicked,
|
||||
screenState = screenState,
|
||||
@@ -356,11 +346,10 @@ fun ConversationsScreen(
|
||||
Modifier
|
||||
}.fillMaxSize(),
|
||||
onOptionClicked = onOptionClicked,
|
||||
padding = padding,
|
||||
onPhotoClicked = onConversationPhotoClicked
|
||||
padding = padding
|
||||
)
|
||||
|
||||
if (screenState.conversations.isEmpty()) {
|
||||
if (conversations.isEmpty()) {
|
||||
NoItemsView(
|
||||
buttonText = stringResource(UiR.string.action_refresh),
|
||||
onButtonClick = onRefresh
|
||||
@@ -371,38 +360,3 @@ fun ConversationsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 26.08.2023, Danil Nikolaev: remove usage of viewModel
|
||||
@Composable
|
||||
fun HandleDialogs(
|
||||
screenState: ConversationsScreenState,
|
||||
viewModel: ConversationsViewModel
|
||||
) {
|
||||
val showOptions = screenState.showOptions
|
||||
|
||||
if (showOptions.showDeleteDialog != null) {
|
||||
MaterialDialog(
|
||||
onDismissRequest = viewModel::onDeleteDialogDismissed,
|
||||
title = stringResource(id = UiR.string.confirm_delete_conversation),
|
||||
confirmAction = viewModel::onDeleteDialogPositiveClick,
|
||||
confirmText = stringResource(id = UiR.string.action_delete),
|
||||
cancelText = stringResource(id = UiR.string.cancel)
|
||||
)
|
||||
}
|
||||
|
||||
showOptions.showPinDialog?.let { conversation ->
|
||||
MaterialDialog(
|
||||
onDismissRequest = viewModel::onPinDialogDismissed,
|
||||
title = stringResource(
|
||||
id = if (conversation.isPinned) UiR.string.confirm_unpin_conversation
|
||||
else UiR.string.confirm_pin_conversation
|
||||
),
|
||||
confirmAction = viewModel::onPinDialogPositiveClick,
|
||||
confirmText = stringResource(
|
||||
id = if (conversation.isPinned) UiR.string.action_unpin
|
||||
else UiR.string.action_pin
|
||||
),
|
||||
cancelText = stringResource(id = UiR.string.cancel)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+31
-17
@@ -23,8 +23,10 @@ import dev.meloda.fast.model.api.domain.VkAttachment
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import dev.meloda.fast.ui.model.api.ActionState
|
||||
import dev.meloda.fast.ui.model.api.ConversationOption
|
||||
import dev.meloda.fast.ui.model.api.UiConversation
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
import dev.meloda.fast.ui.util.emptyImmutableList
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import kotlin.math.ln
|
||||
@@ -33,7 +35,9 @@ import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
fun VkConversation.asPresentation(
|
||||
resources: Resources,
|
||||
useContactName: Boolean
|
||||
useContactName: Boolean,
|
||||
isExpanded: Boolean = false,
|
||||
options: ImmutableList<ConversationOption> = emptyImmutableList()
|
||||
): UiConversation = UiConversation(
|
||||
id = id,
|
||||
lastMessageId = lastMessageId,
|
||||
@@ -47,14 +51,15 @@ fun VkConversation.asPresentation(
|
||||
isPinned = majorId > 0,
|
||||
actionImageId = ActionState.parse(isPhantom, isCallInProgress).getResourceId(),
|
||||
isBirthday = extractBirthday(this),
|
||||
isUnread = extractReadCondition(this, lastMessage),
|
||||
isUnread = !isRead(),
|
||||
isAccount = isAccount(id),
|
||||
isOnline = !isAccount(id) && user?.onlineStatus?.isOnline() == true,
|
||||
lastMessage = lastMessage,
|
||||
peerType = peerType,
|
||||
interactionText = extractInteractionText(resources, this),
|
||||
isExpanded = false,
|
||||
options = ImmutableList.empty()
|
||||
isExpanded = isExpanded,
|
||||
isArchived = isArchived,
|
||||
options = options
|
||||
)
|
||||
|
||||
fun VkConversation.extractAvatar() = when (peerType) {
|
||||
@@ -101,7 +106,7 @@ private fun extractUnreadCount(
|
||||
lastMessage: VkMessage?,
|
||||
conversation: VkConversation
|
||||
): String? = when {
|
||||
lastMessage?.isOut == false && !conversation.isInUnread() -> null
|
||||
lastMessage?.isOut == false && conversation.isInRead() -> null
|
||||
conversation.unreadCount == 0 -> null
|
||||
conversation.unreadCount < 1000 -> conversation.unreadCount.toString()
|
||||
else -> {
|
||||
@@ -121,7 +126,7 @@ private fun extractUnreadCount(
|
||||
private fun extractMessage(
|
||||
resources: Resources,
|
||||
lastMessage: VkMessage?,
|
||||
peerId: Int,
|
||||
peerId: Long,
|
||||
peerType: PeerType
|
||||
): AnnotatedString {
|
||||
val youPrefix = UiText.Resource(UiR.string.you_message_prefix)
|
||||
@@ -210,7 +215,12 @@ private fun extractMessage(
|
||||
.replace("<br/>", " ")
|
||||
.replace("–", "-")
|
||||
.trim()
|
||||
.let { text -> getTextWithVisualizedMentions(text, Color.Red) }
|
||||
.let { text ->
|
||||
extractTextWithVisualizedMentions(
|
||||
isOut = lastMessage?.isOut == true,
|
||||
originalText = text
|
||||
)
|
||||
}
|
||||
.let { text -> prefix + text }
|
||||
}
|
||||
|
||||
@@ -612,6 +622,9 @@ private fun getAttachmentIconByType(attachmentType: AttachmentType): UiImage? {
|
||||
AttachmentType.PODCAST -> null
|
||||
AttachmentType.NARRATIVE -> null
|
||||
AttachmentType.ARTICLE -> null
|
||||
AttachmentType.VIDEO_MESSAGE -> null
|
||||
AttachmentType.GROUP_CHAT_STICKER -> UiR.drawable.ic_attachment_sticker
|
||||
AttachmentType.STICKER_PACK_PREVIEW -> null
|
||||
}?.let(UiImage::Resource)
|
||||
}
|
||||
|
||||
@@ -649,10 +662,9 @@ private fun extractForwardsText(
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
||||
private fun getTextWithVisualizedMentions(
|
||||
originalText: String,
|
||||
mentionColor: Color,
|
||||
fun extractTextWithVisualizedMentions(
|
||||
isOut: Boolean,
|
||||
originalText: String
|
||||
): AnnotatedString = buildAnnotatedString {
|
||||
val regex = """\[(id|club)(\d+)\|([^]]+)]""".toRegex()
|
||||
|
||||
@@ -676,7 +688,7 @@ private fun getTextWithVisualizedMentions(
|
||||
replacements.add(indexRange to replaced)
|
||||
|
||||
mentions += MentionIndex(
|
||||
id = id.toIntOrNull() ?: -1,
|
||||
id = id.toLongOrNull() ?: -1,
|
||||
idPrefix = idPrefix,
|
||||
indexRange = indexRange
|
||||
)
|
||||
@@ -693,7 +705,7 @@ private fun getTextWithVisualizedMentions(
|
||||
val endIndex = mention.indexRange.last
|
||||
|
||||
addStyle(
|
||||
style = SpanStyle(color = mentionColor),
|
||||
style = SpanStyle(color = Color.Red),
|
||||
start = startIndex,
|
||||
end = endIndex
|
||||
)
|
||||
@@ -707,7 +719,7 @@ private fun getTextWithVisualizedMentions(
|
||||
}
|
||||
|
||||
data class MentionIndex(
|
||||
val id: Int,
|
||||
val id: Long,
|
||||
val idPrefix: String,
|
||||
val indexRange: IntRange
|
||||
)
|
||||
@@ -755,6 +767,9 @@ private fun getAttachmentUiText(
|
||||
AttachmentType.PODCAST -> UiR.string.message_attachments_podcast
|
||||
AttachmentType.NARRATIVE -> UiR.string.message_attachments_narrative
|
||||
AttachmentType.ARTICLE -> UiR.string.message_attachments_article
|
||||
AttachmentType.VIDEO_MESSAGE -> UiR.string.message_attachments_video_message
|
||||
AttachmentType.GROUP_CHAT_STICKER -> UiR.string.message_attachments_group_sticker
|
||||
AttachmentType.STICKER_PACK_PREVIEW -> UiR.string.message_attachments_sticker_pack_preview
|
||||
}.let(UiText::Resource)
|
||||
}
|
||||
|
||||
@@ -796,10 +811,9 @@ private fun extractBirthday(conversation: VkConversation): Boolean {
|
||||
private fun extractReadCondition(
|
||||
conversation: VkConversation,
|
||||
lastMessage: VkMessage?
|
||||
): Boolean = (lastMessage?.isOut == true && conversation.isOutUnread()) ||
|
||||
(lastMessage?.isOut == false && conversation.isInUnread())
|
||||
): Boolean = !conversation.isRead(lastMessage)
|
||||
|
||||
private fun isAccount(peerId: Int) = peerId == UserConfig.userId
|
||||
private fun isAccount(peerId: Long) = peerId == UserConfig.userId
|
||||
|
||||
private fun extractInteractionText(
|
||||
resources: Resources,
|
||||
|
||||
Reference in New Issue
Block a user