forked from melod1n/fast-messenger
split MessagesHistoryScreen.kt in separated composables.
Added "regular" to text field in messages history screen - for clearing formatting
This commit is contained in:
@@ -269,6 +269,7 @@
|
|||||||
<string name="italic">Курсив</string>
|
<string name="italic">Курсив</string>
|
||||||
<string name="underline">Подчёркнутый</string>
|
<string name="underline">Подчёркнутый</string>
|
||||||
<string name="link">Ссылка</string>
|
<string name="link">Ссылка</string>
|
||||||
|
<string name="regular">Обычный</string>
|
||||||
<string name="login_sign_up">Регистрация</string>
|
<string name="login_sign_up">Регистрация</string>
|
||||||
<string name="login_forgot_password">Забыли пароль?</string>
|
<string name="login_forgot_password">Забыли пароль?</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -345,6 +345,7 @@
|
|||||||
<string name="italic">Italic</string>
|
<string name="italic">Italic</string>
|
||||||
<string name="underline">Underline</string>
|
<string name="underline">Underline</string>
|
||||||
<string name="link">Link</string>
|
<string name="link">Link</string>
|
||||||
|
<string name="regular">Regular</string>
|
||||||
<string name="login_sign_up">Sign up</string>
|
<string name="login_sign_up">Sign up</string>
|
||||||
<string name="login_forgot_password">Forgot password?</string>
|
<string name="login_forgot_password">Forgot password?</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
+1
-4
@@ -106,10 +106,7 @@ fun ConversationsScreen(
|
|||||||
onErrorViewButtonClicked: () -> Unit = {}
|
onErrorViewButtonClicked: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val currentTheme = LocalThemeConfig.current
|
val currentTheme = LocalThemeConfig.current
|
||||||
|
val maxLines = if (currentTheme.enableMultiline) 2 else 1
|
||||||
val maxLines by remember(currentTheme) {
|
|
||||||
mutableIntStateOf(if (currentTheme.enableMultiline) 2 else 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val listState = rememberLazyListState(
|
val listState = rememberLazyListState(
|
||||||
initialFirstVisibleItemIndex = screenState.scrollIndex,
|
initialFirstVisibleItemIndex = screenState.scrollIndex,
|
||||||
|
|||||||
+6
@@ -111,6 +111,7 @@ interface MessagesHistoryViewModel {
|
|||||||
fun onItalicClicked()
|
fun onItalicClicked()
|
||||||
fun onUnderlineClicked()
|
fun onUnderlineClicked()
|
||||||
fun onLinkClicked()
|
fun onLinkClicked()
|
||||||
|
fun onRegularClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessagesHistoryViewModelImpl(
|
class MessagesHistoryViewModelImpl(
|
||||||
@@ -571,6 +572,11 @@ class MessagesHistoryViewModelImpl(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRegularClicked() {
|
||||||
|
formatData = formatData.copy(items = emptyList())
|
||||||
|
updateStyles()
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
|
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
|
||||||
val message = event.message
|
val message = event.message
|
||||||
|
|
||||||
|
|||||||
+340
@@ -0,0 +1,340 @@
|
|||||||
|
package dev.meloda.fast.messageshistory.presentation
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.imeNestedScroll
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.contextmenu.builder.item
|
||||||
|
import androidx.compose.foundation.text.contextmenu.modifier.addTextContextMenuComponents
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.view.HapticFeedbackConstantsCompat
|
||||||
|
import dev.chrisbanes.haze.HazeState
|
||||||
|
import dev.chrisbanes.haze.hazeEffect
|
||||||
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
|
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||||
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
|
import dev.meloda.fast.messageshistory.model.ActionMode
|
||||||
|
import dev.meloda.fast.ui.components.IconButton
|
||||||
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import dev.meloda.fast.ui.R as UiR
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class, ExperimentalHazeMaterialsApi::class)
|
||||||
|
@Composable
|
||||||
|
fun MessagesHistoryInputBar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
message: TextFieldValue,
|
||||||
|
hazeState: HazeState,
|
||||||
|
showEmojiButton: Boolean,
|
||||||
|
actionMode: ActionMode,
|
||||||
|
onMessageInputChanged: (TextFieldValue) -> Unit = {},
|
||||||
|
onBoldRequested: () -> Unit = {},
|
||||||
|
onItalicRequested: () -> Unit = {},
|
||||||
|
onUnderlineRequested: () -> Unit = {},
|
||||||
|
onLinkRequested: () -> Unit = {},
|
||||||
|
onRegularRequested: () -> Unit = {},
|
||||||
|
onSetMessageBarHeight: (Dp) -> Unit = {},
|
||||||
|
onEmojiButtonLongClicked: () -> Unit = {},
|
||||||
|
onAttachmentButtonClicked: () -> Unit = {},
|
||||||
|
onActionButtonClicked: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
val theme = LocalThemeConfig.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color.Transparent)
|
||||||
|
.padding(bottom = 8.dp)
|
||||||
|
.navigationBarsPadding()
|
||||||
|
.imePadding()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.defaultMinSize(minHeight = 60.dp)
|
||||||
|
.imeNestedScroll(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(36.dp))
|
||||||
|
.then(
|
||||||
|
if (theme.enableBlur) {
|
||||||
|
Modifier
|
||||||
|
.hazeEffect(
|
||||||
|
state = hazeState,
|
||||||
|
style = HazeMaterials.ultraThin()
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
1.dp, MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
RoundedCornerShape(36.dp)
|
||||||
|
)
|
||||||
|
} else Modifier
|
||||||
|
)
|
||||||
|
.animateContentSize()
|
||||||
|
.weight(1f)
|
||||||
|
.background(
|
||||||
|
if (theme.enableBlur) Color.Transparent
|
||||||
|
else MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp)
|
||||||
|
)
|
||||||
|
.onGloballyPositioned {
|
||||||
|
with(density) {
|
||||||
|
onSetMessageBarHeight(it.size.height.toDp())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.Bottom
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
|
||||||
|
if (showEmojiButton) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val rotation = remember { Animatable(0f) }
|
||||||
|
|
||||||
|
Column(verticalArrangement = Arrangement.Bottom) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (AppSettings.General.enableHaptic) {
|
||||||
|
view.performHapticFeedback(
|
||||||
|
HapticFeedbackConstantsCompat.REJECT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
scope.launch {
|
||||||
|
for (i in 20 downTo 0 step 4) {
|
||||||
|
rotation.animateTo(
|
||||||
|
targetValue = i.toFloat(),
|
||||||
|
animationSpec = tween(50)
|
||||||
|
)
|
||||||
|
if (i > 0) {
|
||||||
|
rotation.animateTo(
|
||||||
|
targetValue = -i.toFloat(),
|
||||||
|
animationSpec = tween(50)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
if (AppSettings.General.enableHaptic) {
|
||||||
|
view.performHapticFeedback(
|
||||||
|
HapticFeedbackConstantsCompat.LONG_PRESS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onEmojiButtonLongClicked()
|
||||||
|
},
|
||||||
|
modifier = Modifier.rotate(rotation.value)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = UiR.drawable.ic_outline_emoji_emotions_24),
|
||||||
|
contentDescription = "Emoji button",
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
modifier = modifier
|
||||||
|
.weight(1f)
|
||||||
|
.addTextContextMenuComponents {
|
||||||
|
separator()
|
||||||
|
|
||||||
|
item(
|
||||||
|
key = "Bold",
|
||||||
|
label = context.getString(UiR.string.bold)
|
||||||
|
) {
|
||||||
|
onBoldRequested()
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
item(
|
||||||
|
key = "Italic",
|
||||||
|
label = context.getString(UiR.string.italic)
|
||||||
|
) {
|
||||||
|
onItalicRequested()
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
item(
|
||||||
|
key = "Underline",
|
||||||
|
label = context.getString(UiR.string.underline)
|
||||||
|
) {
|
||||||
|
onUnderlineRequested()
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
item(
|
||||||
|
key = "Link",
|
||||||
|
label = context.getString(UiR.string.link)
|
||||||
|
) {
|
||||||
|
onLinkRequested()
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
item(
|
||||||
|
key = "Regular",
|
||||||
|
label = context.getString(UiR.string.regular)
|
||||||
|
) {
|
||||||
|
onRegularRequested()
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
separator()
|
||||||
|
},
|
||||||
|
value = message,
|
||||||
|
onValueChange = onMessageInputChanged,
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
unfocusedContainerColor = Color.Transparent,
|
||||||
|
focusedContainerColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = UiR.string.message_input_hint),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val attachmentRotation = remember { Animatable(0f) }
|
||||||
|
|
||||||
|
Column(verticalArrangement = Arrangement.Bottom) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
onAttachmentButtonClicked()
|
||||||
|
if (AppSettings.General.enableHaptic) {
|
||||||
|
view.performHapticFeedback(
|
||||||
|
HapticFeedbackConstantsCompat.REJECT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
scope.launch {
|
||||||
|
for (i in 20 downTo 0 step 4) {
|
||||||
|
attachmentRotation.animateTo(
|
||||||
|
targetValue = i.toFloat(),
|
||||||
|
animationSpec = tween(50)
|
||||||
|
)
|
||||||
|
if (i > 0) {
|
||||||
|
attachmentRotation.animateTo(
|
||||||
|
targetValue = -i.toFloat(),
|
||||||
|
animationSpec = tween(50)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = UiR.drawable.round_attach_file_24),
|
||||||
|
contentDescription = "Add attachment button",
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.rotate(30f + attachmentRotation.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
val micRotation = remember { Animatable(0f) }
|
||||||
|
|
||||||
|
Column(verticalArrangement = Arrangement.Bottom) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (actionMode == ActionMode.Record) {
|
||||||
|
if (AppSettings.General.enableHaptic) {
|
||||||
|
view.performHapticFeedback(
|
||||||
|
HapticFeedbackConstantsCompat.REJECT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
scope.launch {
|
||||||
|
for (i in 20 downTo 0 step 4) {
|
||||||
|
micRotation.animateTo(
|
||||||
|
targetValue = i.toFloat(),
|
||||||
|
animationSpec = tween(50)
|
||||||
|
)
|
||||||
|
if (i > 0) {
|
||||||
|
micRotation.animateTo(
|
||||||
|
targetValue = -i.toFloat(),
|
||||||
|
animationSpec = tween(50)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onActionButtonClicked()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.rotate(micRotation.value)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(
|
||||||
|
id = when (actionMode) {
|
||||||
|
ActionMode.Delete -> UiR.drawable.round_delete_outline_24
|
||||||
|
ActionMode.Edit -> UiR.drawable.ic_round_done_24
|
||||||
|
ActionMode.Record -> UiR.drawable.ic_round_mic_none_24
|
||||||
|
ActionMode.Send -> UiR.drawable.round_send_24
|
||||||
|
}
|
||||||
|
),
|
||||||
|
contentDescription = when (actionMode) {
|
||||||
|
ActionMode.Delete -> "Delete message button"
|
||||||
|
ActionMode.Edit -> "Edit message button"
|
||||||
|
ActionMode.Record -> "Record audio message button"
|
||||||
|
ActionMode.Send -> "Send message button"
|
||||||
|
},
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-1
@@ -73,7 +73,9 @@ fun MessagesHistoryRoute(
|
|||||||
onDeleteSelectedButtonClicked = viewModel::onDeleteSelectedMessagesClicked,
|
onDeleteSelectedButtonClicked = viewModel::onDeleteSelectedMessagesClicked,
|
||||||
onBoldRequested = viewModel::onBoldClicked,
|
onBoldRequested = viewModel::onBoldClicked,
|
||||||
onItalicRequested = viewModel::onItalicClicked,
|
onItalicRequested = viewModel::onItalicClicked,
|
||||||
onUnderlineRequested = viewModel::onUnderlineClicked
|
onUnderlineRequested = viewModel::onUnderlineClicked,
|
||||||
|
onLinkRequested = viewModel::onLinkClicked,
|
||||||
|
onRegularRequested = viewModel::onRegularClicked
|
||||||
)
|
)
|
||||||
|
|
||||||
HandleDialogs(
|
HandleDialogs(
|
||||||
|
|||||||
+50
-524
@@ -1,62 +1,23 @@
|
|||||||
package dev.meloda.fast.messageshistory.presentation
|
package dev.meloda.fast.messageshistory.presentation
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.Crossfade
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.animateContentSize
|
|
||||||
import androidx.compose.animation.core.Animatable
|
|
||||||
import androidx.compose.animation.core.FastOutLinearInEasing
|
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.calculateEndPadding
|
import androidx.compose.foundation.layout.calculateEndPadding
|
||||||
import androidx.compose.foundation.layout.calculateStartPadding
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.imeNestedScroll
|
|
||||||
import androidx.compose.foundation.layout.imePadding
|
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.contextmenu.builder.item
|
|
||||||
import androidx.compose.foundation.text.contextmenu.modifier.addTextContextMenuComponents
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
|
||||||
import androidx.compose.material.icons.outlined.MoreVert
|
|
||||||
import androidx.compose.material.icons.rounded.Close
|
|
||||||
import androidx.compose.material.icons.rounded.Refresh
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -68,42 +29,25 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.draw.rotate
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.DpOffset
|
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.view.HapticFeedbackConstantsCompat
|
import androidx.core.view.HapticFeedbackConstantsCompat
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.hazeEffect
|
|
||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
|
||||||
import dev.meloda.fast.common.extensions.orDots
|
|
||||||
import dev.meloda.fast.data.UserConfig
|
import dev.meloda.fast.data.UserConfig
|
||||||
import dev.meloda.fast.datastore.AppSettings
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
import dev.meloda.fast.messageshistory.model.ActionMode
|
|
||||||
import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState
|
import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState
|
||||||
import dev.meloda.fast.messageshistory.model.UiItem
|
import dev.meloda.fast.messageshistory.model.UiItem
|
||||||
import dev.meloda.fast.messageshistory.util.indexOfMessageByCmId
|
import dev.meloda.fast.messageshistory.util.indexOfMessageByCmId
|
||||||
import dev.meloda.fast.model.BaseError
|
import dev.meloda.fast.model.BaseError
|
||||||
import dev.meloda.fast.model.api.domain.VkMessage
|
import dev.meloda.fast.model.api.domain.VkMessage
|
||||||
import dev.meloda.fast.ui.components.IconButton
|
|
||||||
import dev.meloda.fast.ui.components.VkErrorView
|
import dev.meloda.fast.ui.components.VkErrorView
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
import dev.meloda.fast.ui.util.ImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList
|
||||||
import dev.meloda.fast.ui.util.emptyImmutableList
|
import dev.meloda.fast.ui.util.emptyImmutableList
|
||||||
import dev.meloda.fast.ui.util.getImage
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import dev.meloda.fast.ui.R as UiR
|
import dev.meloda.fast.ui.R as UiR
|
||||||
|
|
||||||
@@ -140,7 +84,9 @@ fun MessagesHistoryScreen(
|
|||||||
onDeleteSelectedButtonClicked: () -> Unit = {},
|
onDeleteSelectedButtonClicked: () -> Unit = {},
|
||||||
onBoldRequested: () -> Unit = {},
|
onBoldRequested: () -> Unit = {},
|
||||||
onItalicRequested: () -> Unit = {},
|
onItalicRequested: () -> Unit = {},
|
||||||
|
onLinkRequested: () -> Unit = {},
|
||||||
onUnderlineRequested: () -> Unit = {},
|
onUnderlineRequested: () -> Unit = {},
|
||||||
|
onRegularRequested: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
@@ -179,10 +125,6 @@ fun MessagesHistoryScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var dropDownMenuExpanded by remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
val topBarContainerColorAlpha by animateFloatAsState(
|
val topBarContainerColorAlpha by animateFloatAsState(
|
||||||
targetValue = if (!theme.enableBlur || !listState.canScrollBackward) 1f else 0f,
|
targetValue = if (!theme.enableBlur || !listState.canScrollBackward) 1f else 0f,
|
||||||
label = "toolbarColorAlpha",
|
label = "toolbarColorAlpha",
|
||||||
@@ -207,8 +149,6 @@ fun MessagesHistoryScreen(
|
|||||||
mutableStateOf(0.dp)
|
mutableStateOf(0.dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
val density = LocalDensity.current
|
|
||||||
|
|
||||||
val showReplyAction by remember(selectedMessages) {
|
val showReplyAction by remember(selectedMessages) {
|
||||||
derivedStateOf { selectedMessages.size == 1 }
|
derivedStateOf { selectedMessages.size == 1 }
|
||||||
}
|
}
|
||||||
@@ -217,223 +157,40 @@ fun MessagesHistoryScreen(
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentWindowInsets = WindowInsets.statusBars,
|
contentWindowInsets = WindowInsets.statusBars,
|
||||||
topBar = {
|
topBar = {
|
||||||
Column(
|
val topBarTitle by remember(screenState, selectedMessages) {
|
||||||
modifier = Modifier
|
derivedStateOf {
|
||||||
.fillMaxWidth()
|
when {
|
||||||
.background(topBarContainerColor.copy(alpha = topBarContainerColorAlpha))
|
screenState.isLoading -> context.getString(UiR.string.title_loading)
|
||||||
.then(
|
selectedMessages.isNotEmpty() -> "(${selectedMessages.size})"
|
||||||
if (theme.enableBlur) {
|
else -> screenState.title
|
||||||
Modifier.hazeEffect(
|
|
||||||
state = hazeState,
|
|
||||||
style = HazeMaterials.thick()
|
|
||||||
)
|
|
||||||
} else Modifier
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
TopAppBar(
|
|
||||||
modifier = Modifier
|
|
||||||
.then(
|
|
||||||
if (theme.enableBlur) {
|
|
||||||
Modifier.hazeEffect(
|
|
||||||
state = hazeState,
|
|
||||||
style = HazeMaterials.thick()
|
|
||||||
)
|
|
||||||
} else Modifier
|
|
||||||
)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.then(
|
|
||||||
if (screenState.isLoading && messages.isEmpty()) Modifier
|
|
||||||
else Modifier.clickable {
|
|
||||||
onTopBarClicked()
|
|
||||||
}
|
|
||||||
),
|
|
||||||
title = {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
if (selectedMessages.isEmpty()) {
|
|
||||||
val avatar = screenState.avatar.getImage()
|
|
||||||
if (screenState.conversationId == UserConfig.userId) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(36.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(MaterialTheme.colorScheme.primary)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.Center)
|
|
||||||
.size(24.dp),
|
|
||||||
painter = painterResource(id = UiR.drawable.ic_round_bookmark_border_24),
|
|
||||||
contentDescription = "Favorites icon",
|
|
||||||
tint = MaterialTheme.colorScheme.onPrimary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (avatar is Painter) {
|
|
||||||
Image(
|
|
||||||
painter = avatar,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(36.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
AsyncImage(
|
|
||||||
model = screenState.avatar.getImage(),
|
|
||||||
contentDescription = "Profile Image",
|
|
||||||
modifier = Modifier
|
|
||||||
.size(36.dp)
|
|
||||||
.clip(CircleShape),
|
|
||||||
placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = when {
|
|
||||||
screenState.isLoading -> stringResource(id = UiR.string.title_loading)
|
|
||||||
selectedMessages.isNotEmpty() -> "(${selectedMessages.size})"
|
|
||||||
else -> screenState.title
|
|
||||||
},
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
style = MaterialTheme.typography.headlineSmall
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
if (selectedMessages.isEmpty()) onBack()
|
|
||||||
else onClose()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Crossfade(targetState = selectedMessages.isEmpty()) { state ->
|
|
||||||
Icon(
|
|
||||||
imageVector = if (state) {
|
|
||||||
Icons.AutoMirrored.Rounded.ArrowBack
|
|
||||||
} else {
|
|
||||||
Icons.Rounded.Close
|
|
||||||
},
|
|
||||||
contentDescription = if (state) "Close button"
|
|
||||||
else "Back button"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
|
|
||||||
actions = {
|
|
||||||
if (selectedMessages.isNotEmpty()) {
|
|
||||||
AnimatedVisibility(showReplyAction) {
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
if (AppSettings.General.enableHaptic) {
|
|
||||||
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(UiR.drawable.round_reply_24),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
if (AppSettings.General.enableHaptic) {
|
|
||||||
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(UiR.drawable.round_reply_all_24),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
if (AppSettings.General.enableHaptic) {
|
|
||||||
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(UiR.drawable.round_forward_24),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(onClick = onDeleteSelectedButtonClicked) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(UiR.drawable.round_delete_outline_24),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
IconButton(
|
|
||||||
onClick = { dropDownMenuExpanded = true }
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.MoreVert,
|
|
||||||
contentDescription = "Options"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
DropdownMenu(
|
|
||||||
modifier = Modifier.defaultMinSize(minWidth = 140.dp),
|
|
||||||
expanded = dropDownMenuExpanded,
|
|
||||||
onDismissRequest = {
|
|
||||||
dropDownMenuExpanded = false
|
|
||||||
},
|
|
||||||
offset = DpOffset(x = (-4).dp, y = (-60).dp)
|
|
||||||
) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
onClick = {
|
|
||||||
onRefresh()
|
|
||||||
dropDownMenuExpanded = false
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(text = stringResource(UiR.string.action_refresh))
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.Refresh,
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
val showHorizontalProgressBar by remember(screenState) {
|
|
||||||
derivedStateOf { screenState.isLoading && messages.isNotEmpty() }
|
|
||||||
}
|
|
||||||
if (showHorizontalProgressBar) {
|
|
||||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
|
||||||
}
|
|
||||||
AnimatedVisibility(!showHorizontalProgressBar) {
|
|
||||||
HorizontalDivider()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!screenState.isLoading && pinnedMessage != null) {
|
|
||||||
PinnedMessageContainer(
|
|
||||||
modifier = Modifier,
|
|
||||||
pinnedMessage = requireNotNull(pinnedMessage),
|
|
||||||
title = screenState.pinnedTitle.orDots(),
|
|
||||||
summary = screenState.pinnedSummary,
|
|
||||||
canChangePin = screenState.conversation.canChangePin,
|
|
||||||
onPinnedMessageClicked = onPinnedMessageClicked,
|
|
||||||
onUnpinMessageButtonClicked = onUnpinMessageButtonClicked
|
|
||||||
)
|
|
||||||
HorizontalDivider()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MessagesHistoryTopBarContainer(
|
||||||
|
hazeState = hazeState,
|
||||||
|
showReplyAction = showReplyAction,
|
||||||
|
topBarContainerColor = topBarContainerColor,
|
||||||
|
topBarContainerColorAlpha = topBarContainerColorAlpha,
|
||||||
|
isClickable = !(screenState.isLoading && messages.isEmpty()),
|
||||||
|
isMessagesSelecting = selectedMessages.isNotEmpty(),
|
||||||
|
isPeerAccount = screenState.conversationId == UserConfig.userId,
|
||||||
|
avatar = screenState.avatar,
|
||||||
|
title = topBarTitle,
|
||||||
|
showHorizontalProgressBar = screenState.isLoading && messages.isNotEmpty(),
|
||||||
|
showPinnedContainer = !screenState.isLoading && pinnedMessage != null,
|
||||||
|
pinnedMessage = pinnedMessage,
|
||||||
|
pinnedTitle = screenState.pinnedTitle,
|
||||||
|
pinnedSummary = screenState.pinnedSummary,
|
||||||
|
showUnpinButton = screenState.conversation.canChangePin,
|
||||||
|
onTopBarClicked = onTopBarClicked,
|
||||||
|
onBack = onBack,
|
||||||
|
onClose = onClose,
|
||||||
|
onDeleteSelectedButtonClicked = onDeleteSelectedButtonClicked,
|
||||||
|
onRefresh = onRefresh,
|
||||||
|
onPinnedMessageClicked = onPinnedMessageClicked,
|
||||||
|
onUnpinMessageButtonClicked = onUnpinMessageButtonClicked
|
||||||
|
)
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Box(
|
Box(
|
||||||
@@ -472,254 +229,23 @@ fun MessagesHistoryScreen(
|
|||||||
onMessageLongClicked = onMessageLongClicked
|
onMessageLongClicked = onMessageLongClicked
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(
|
MessagesHistoryInputBar(
|
||||||
modifier = Modifier
|
modifier = Modifier.align(Alignment.BottomStart),
|
||||||
.fillMaxWidth()
|
message = screenState.message,
|
||||||
.align(Alignment.BottomStart)
|
onMessageInputChanged = onMessageInputChanged,
|
||||||
.background(Color.Transparent)
|
onBoldRequested = onBoldRequested,
|
||||||
.padding(bottom = 8.dp)
|
onItalicRequested = onItalicRequested,
|
||||||
.navigationBarsPadding()
|
onUnderlineRequested = onUnderlineRequested,
|
||||||
.imePadding()
|
onLinkRequested = onLinkRequested,
|
||||||
) {
|
onRegularRequested = onRegularRequested,
|
||||||
Row(
|
hazeState = hazeState,
|
||||||
modifier = Modifier
|
showEmojiButton = showEmojiButton,
|
||||||
.fillMaxWidth()
|
actionMode = screenState.actionMode,
|
||||||
.defaultMinSize(minHeight = 60.dp)
|
onSetMessageBarHeight = { messageBarHeight = it },
|
||||||
.imeNestedScroll(),
|
onEmojiButtonLongClicked = onEmojiButtonLongClicked,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
onAttachmentButtonClicked = onAttachmentButtonClicked,
|
||||||
) {
|
onActionButtonClicked = onActionButtonClicked
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
)
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(RoundedCornerShape(36.dp))
|
|
||||||
.then(
|
|
||||||
if (theme.enableBlur) {
|
|
||||||
Modifier
|
|
||||||
.hazeEffect(
|
|
||||||
state = hazeState,
|
|
||||||
style = HazeMaterials.ultraThin()
|
|
||||||
)
|
|
||||||
.border(
|
|
||||||
1.dp, MaterialTheme.colorScheme.outlineVariant,
|
|
||||||
RoundedCornerShape(36.dp)
|
|
||||||
)
|
|
||||||
} else Modifier
|
|
||||||
)
|
|
||||||
.animateContentSize()
|
|
||||||
.weight(1f)
|
|
||||||
.background(
|
|
||||||
if (theme.enableBlur) Color.Transparent
|
|
||||||
else MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp)
|
|
||||||
)
|
|
||||||
.onGloballyPositioned {
|
|
||||||
messageBarHeight = with(density) {
|
|
||||||
it.size.height.toDp()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.Bottom
|
|
||||||
) {
|
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
|
||||||
|
|
||||||
if (showEmojiButton) {
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val rotation = remember { Animatable(0f) }
|
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.Bottom) {
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
if (AppSettings.General.enableHaptic) {
|
|
||||||
view.performHapticFeedback(
|
|
||||||
HapticFeedbackConstantsCompat.REJECT
|
|
||||||
)
|
|
||||||
}
|
|
||||||
scope.launch {
|
|
||||||
for (i in 20 downTo 0 step 4) {
|
|
||||||
rotation.animateTo(
|
|
||||||
targetValue = i.toFloat(),
|
|
||||||
animationSpec = tween(50)
|
|
||||||
)
|
|
||||||
if (i > 0) {
|
|
||||||
rotation.animateTo(
|
|
||||||
targetValue = -i.toFloat(),
|
|
||||||
animationSpec = tween(50)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongClick = {
|
|
||||||
if (AppSettings.General.enableHaptic) {
|
|
||||||
view.performHapticFeedback(
|
|
||||||
HapticFeedbackConstantsCompat.LONG_PRESS
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onEmojiButtonLongClicked()
|
|
||||||
},
|
|
||||||
modifier = Modifier.rotate(rotation.value)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = UiR.drawable.ic_outline_emoji_emotions_24),
|
|
||||||
contentDescription = "Emoji button",
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextField(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.addTextContextMenuComponents {
|
|
||||||
separator()
|
|
||||||
|
|
||||||
item(
|
|
||||||
key = "Bold",
|
|
||||||
label = context.getString(UiR.string.bold)
|
|
||||||
) {
|
|
||||||
onBoldRequested()
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
item(
|
|
||||||
key = "Italic",
|
|
||||||
label = context.getString(UiR.string.italic)
|
|
||||||
) {
|
|
||||||
onItalicRequested()
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
item(
|
|
||||||
key = "Underline",
|
|
||||||
label = context.getString(UiR.string.underline)
|
|
||||||
) {
|
|
||||||
onUnderlineRequested()
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
item(
|
|
||||||
key = "Link",
|
|
||||||
label = context.getString(UiR.string.link)
|
|
||||||
) {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
separator()
|
|
||||||
},
|
|
||||||
value = screenState.message,
|
|
||||||
onValueChange = onMessageInputChanged,
|
|
||||||
colors = TextFieldDefaults.colors(
|
|
||||||
unfocusedContainerColor = Color.Transparent,
|
|
||||||
focusedContainerColor = Color.Transparent,
|
|
||||||
unfocusedIndicatorColor = Color.Transparent,
|
|
||||||
focusedIndicatorColor = Color.Transparent,
|
|
||||||
),
|
|
||||||
placeholder = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = UiR.string.message_input_hint),
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val attachmentRotation = remember { Animatable(0f) }
|
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.Bottom) {
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
onAttachmentButtonClicked()
|
|
||||||
if (AppSettings.General.enableHaptic) {
|
|
||||||
view.performHapticFeedback(
|
|
||||||
HapticFeedbackConstantsCompat.REJECT
|
|
||||||
)
|
|
||||||
}
|
|
||||||
scope.launch {
|
|
||||||
for (i in 20 downTo 0 step 4) {
|
|
||||||
attachmentRotation.animateTo(
|
|
||||||
targetValue = i.toFloat(),
|
|
||||||
animationSpec = tween(50)
|
|
||||||
)
|
|
||||||
if (i > 0) {
|
|
||||||
attachmentRotation.animateTo(
|
|
||||||
targetValue = -i.toFloat(),
|
|
||||||
animationSpec = tween(50)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = UiR.drawable.round_attach_file_24),
|
|
||||||
contentDescription = "Add attachment button",
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.rotate(30f + attachmentRotation.value)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
val micRotation = remember { Animatable(0f) }
|
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.Bottom) {
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
if (screenState.actionMode == ActionMode.Record) {
|
|
||||||
if (AppSettings.General.enableHaptic) {
|
|
||||||
view.performHapticFeedback(
|
|
||||||
HapticFeedbackConstantsCompat.REJECT
|
|
||||||
)
|
|
||||||
}
|
|
||||||
scope.launch {
|
|
||||||
for (i in 20 downTo 0 step 4) {
|
|
||||||
micRotation.animateTo(
|
|
||||||
targetValue = i.toFloat(),
|
|
||||||
animationSpec = tween(50)
|
|
||||||
)
|
|
||||||
if (i > 0) {
|
|
||||||
micRotation.animateTo(
|
|
||||||
targetValue = -i.toFloat(),
|
|
||||||
animationSpec = tween(50)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
onActionButtonClicked()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.rotate(micRotation.value)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(
|
|
||||||
id = when (screenState.actionMode) {
|
|
||||||
ActionMode.Delete -> UiR.drawable.round_delete_outline_24
|
|
||||||
ActionMode.Edit -> UiR.drawable.ic_round_done_24
|
|
||||||
ActionMode.Record -> UiR.drawable.ic_round_mic_none_24
|
|
||||||
ActionMode.Send -> UiR.drawable.round_send_24
|
|
||||||
}
|
|
||||||
),
|
|
||||||
contentDescription = when (screenState.actionMode) {
|
|
||||||
ActionMode.Delete -> "Delete message button"
|
|
||||||
ActionMode.Edit -> "Edit message button"
|
|
||||||
ActionMode.Record -> "Record audio message button"
|
|
||||||
ActionMode.Send -> "Send message button"
|
|
||||||
},
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(6.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(10.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
when {
|
when {
|
||||||
screenState.isLoading && messages.values.isEmpty() -> {
|
screenState.isLoading && messages.values.isEmpty() -> {
|
||||||
|
|||||||
+260
@@ -0,0 +1,260 @@
|
|||||||
|
package dev.meloda.fast.messageshistory.presentation
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||||
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
|
import androidx.compose.material.icons.rounded.Close
|
||||||
|
import androidx.compose.material.icons.rounded.Refresh
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
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
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.view.HapticFeedbackConstantsCompat
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import dev.chrisbanes.haze.HazeState
|
||||||
|
import dev.chrisbanes.haze.hazeEffect
|
||||||
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
|
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||||
|
import dev.meloda.fast.common.model.UiImage
|
||||||
|
import dev.meloda.fast.datastore.AppSettings
|
||||||
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
|
import dev.meloda.fast.ui.util.getImage
|
||||||
|
|
||||||
|
import dev.meloda.fast.ui.R as UiR
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
||||||
|
@Composable
|
||||||
|
fun MessagesHistoryTopBar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
hazeState: HazeState,
|
||||||
|
showReplyAction: Boolean,
|
||||||
|
isClickable: Boolean,
|
||||||
|
isMessagesSelecting: Boolean,
|
||||||
|
isPeerAccount: Boolean,
|
||||||
|
avatar: UiImage,
|
||||||
|
title: String,
|
||||||
|
onTopBarClicked: () -> Unit = {},
|
||||||
|
onBack: () -> Unit = {},
|
||||||
|
onClose: () -> Unit = {},
|
||||||
|
onDeleteSelectedButtonClicked: () -> Unit = {},
|
||||||
|
onRefresh: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val view = LocalView.current
|
||||||
|
val theme = LocalThemeConfig.current
|
||||||
|
|
||||||
|
var dropDownMenuExpanded by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
TopAppBar(
|
||||||
|
modifier = modifier
|
||||||
|
.then(
|
||||||
|
if (theme.enableBlur) {
|
||||||
|
Modifier.hazeEffect(
|
||||||
|
state = hazeState,
|
||||||
|
style = HazeMaterials.thick()
|
||||||
|
)
|
||||||
|
} else Modifier
|
||||||
|
)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.then(
|
||||||
|
if (!isClickable) Modifier
|
||||||
|
else Modifier.clickable {
|
||||||
|
onTopBarClicked()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
title = {
|
||||||
|
Row(
|
||||||
|
// modifier = Modifier.weight(1f),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (!isMessagesSelecting) {
|
||||||
|
if (isPeerAccount) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(36.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.primary)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.size(24.dp),
|
||||||
|
painter = painterResource(id = UiR.drawable.ic_round_bookmark_border_24),
|
||||||
|
contentDescription = "Favorites icon",
|
||||||
|
tint = MaterialTheme.colorScheme.onPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val actualAvatar = avatar.getImage()
|
||||||
|
|
||||||
|
if (actualAvatar is Painter) {
|
||||||
|
Image(
|
||||||
|
painter = actualAvatar,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(36.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AsyncImage(
|
||||||
|
model = actualAvatar,
|
||||||
|
contentDescription = "Profile Image",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(36.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (!isMessagesSelecting) onBack()
|
||||||
|
else onClose()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Crossfade(targetState = !isMessagesSelecting) { state ->
|
||||||
|
Icon(
|
||||||
|
imageVector = if (state) {
|
||||||
|
Icons.AutoMirrored.Rounded.ArrowBack
|
||||||
|
} else {
|
||||||
|
Icons.Rounded.Close
|
||||||
|
},
|
||||||
|
contentDescription = if (state) "Close button"
|
||||||
|
else "Back button"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
|
||||||
|
actions = {
|
||||||
|
if (isMessagesSelecting) {
|
||||||
|
AnimatedVisibility(showReplyAction) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (AppSettings.General.enableHaptic) {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(UiR.drawable.round_reply_24),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (AppSettings.General.enableHaptic) {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(UiR.drawable.round_reply_all_24),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (AppSettings.General.enableHaptic) {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(UiR.drawable.round_forward_24),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = onDeleteSelectedButtonClicked) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(UiR.drawable.round_delete_outline_24),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
IconButton(
|
||||||
|
onClick = { dropDownMenuExpanded = true }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.MoreVert,
|
||||||
|
contentDescription = "Options"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
modifier = Modifier.defaultMinSize(minWidth = 140.dp),
|
||||||
|
expanded = dropDownMenuExpanded,
|
||||||
|
onDismissRequest = {
|
||||||
|
dropDownMenuExpanded = false
|
||||||
|
},
|
||||||
|
offset = DpOffset(x = (-4).dp, y = (-60).dp)
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
onClick = {
|
||||||
|
onRefresh()
|
||||||
|
dropDownMenuExpanded = false
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(UiR.string.action_refresh))
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Refresh,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
package dev.meloda.fast.messageshistory.presentation
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import dev.chrisbanes.haze.HazeState
|
||||||
|
import dev.chrisbanes.haze.hazeEffect
|
||||||
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
|
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||||
|
import dev.meloda.fast.common.extensions.orDots
|
||||||
|
import dev.meloda.fast.common.model.UiImage
|
||||||
|
import dev.meloda.fast.model.api.domain.VkMessage
|
||||||
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
|
|
||||||
|
@OptIn(ExperimentalHazeMaterialsApi::class, ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun MessagesHistoryTopBarContainer(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
hazeState: HazeState,
|
||||||
|
showReplyAction: Boolean,
|
||||||
|
topBarContainerColor: Color,
|
||||||
|
topBarContainerColorAlpha: Float,
|
||||||
|
isClickable: Boolean,
|
||||||
|
isMessagesSelecting: Boolean,
|
||||||
|
isPeerAccount: Boolean,
|
||||||
|
avatar: UiImage,
|
||||||
|
title: String,
|
||||||
|
showHorizontalProgressBar: Boolean,
|
||||||
|
showPinnedContainer: Boolean,
|
||||||
|
pinnedMessage: VkMessage?,
|
||||||
|
pinnedTitle: String?,
|
||||||
|
pinnedSummary: AnnotatedString?,
|
||||||
|
showUnpinButton: Boolean,
|
||||||
|
onTopBarClicked: () -> Unit = {},
|
||||||
|
onBack: () -> Unit = {},
|
||||||
|
onClose: () -> Unit = {},
|
||||||
|
onDeleteSelectedButtonClicked: () -> Unit = {},
|
||||||
|
onRefresh: () -> Unit = {},
|
||||||
|
onPinnedMessageClicked: (Long) -> Unit = {},
|
||||||
|
onUnpinMessageButtonClicked: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val theme = LocalThemeConfig.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(topBarContainerColor.copy(alpha = topBarContainerColorAlpha))
|
||||||
|
.then(
|
||||||
|
if (theme.enableBlur) {
|
||||||
|
Modifier.hazeEffect(
|
||||||
|
state = hazeState,
|
||||||
|
style = HazeMaterials.thick()
|
||||||
|
)
|
||||||
|
} else Modifier
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
MessagesHistoryTopBar(
|
||||||
|
modifier = modifier,
|
||||||
|
hazeState = hazeState,
|
||||||
|
showReplyAction = showReplyAction,
|
||||||
|
isClickable = isClickable,
|
||||||
|
isMessagesSelecting = isMessagesSelecting,
|
||||||
|
isPeerAccount = isPeerAccount,
|
||||||
|
avatar = avatar,
|
||||||
|
title = title,
|
||||||
|
onTopBarClicked = onTopBarClicked,
|
||||||
|
onBack = onBack,
|
||||||
|
onClose = onClose,
|
||||||
|
onDeleteSelectedButtonClicked = onDeleteSelectedButtonClicked,
|
||||||
|
onRefresh = onRefresh
|
||||||
|
)
|
||||||
|
|
||||||
|
if (showHorizontalProgressBar) {
|
||||||
|
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||||
|
}
|
||||||
|
AnimatedVisibility(!showHorizontalProgressBar) {
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showPinnedContainer) {
|
||||||
|
PinnedMessageContainer(
|
||||||
|
modifier = Modifier,
|
||||||
|
pinnedMessage = requireNotNull(pinnedMessage),
|
||||||
|
title = pinnedTitle.orDots(),
|
||||||
|
summary = pinnedSummary,
|
||||||
|
canChangePin = showUnpinButton,
|
||||||
|
onPinnedMessageClicked = onPinnedMessageClicked,
|
||||||
|
onUnpinMessageButtonClicked = onUnpinMessageButtonClicked
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user