Почти плавный скрол чата
This commit is contained in:
@@ -61,6 +61,10 @@ android {
|
|||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion = "1.5.8"
|
kotlinCompilerExtensionVersion = "1.5.8"
|
||||||
}
|
}
|
||||||
|
// Включаем оптимизации компилятора Compose
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
packaging {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="core.notifications.data.ForkFirebaseMessagingService"
|
android:name="core.notifications.data.KnotFirebaseMessagingService"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import androidx.compose.material3.Surface
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import navigation.AppNavigation
|
import navigation.AppNavigation
|
||||||
import core.presentation.theme.ForkMessengerTheme
|
import core.presentation.theme.KnotTheme
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@@ -38,7 +38,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
ForkMessengerTheme {
|
KnotTheme {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = MaterialTheme.colorScheme.background
|
color = MaterialTheme.colorScheme.background
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<!-- Цветная иконка для notification -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#6C5CE7"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M12,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6 6,-2.69 6,-6 -2.69,-6 -6,-6zM12,16c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4z"/>
|
||||||
|
</vector>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">ForkMessenger</string>
|
<string name="app_name">Knot</string>
|
||||||
<string name="login">Login</string>
|
<string name="login">Login</string>
|
||||||
<string name="register">Register</string>
|
<string name="register">Register</string>
|
||||||
<string name="username">Username</string>
|
<string name="username">Username</string>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">ForkMessenger</string>
|
<string name="app_name">Knot</string>
|
||||||
<string name="login">Войти</string>
|
<string name="login">Войти</string>
|
||||||
<string name="register">Регистрация</string>
|
<string name="register">Регистрация</string>
|
||||||
<string name="username">Имя пользователя</string>
|
<string name="username">Имя пользователя</string>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package chats.domain.model
|
package chats.domain.model
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
|
||||||
|
@Immutable
|
||||||
data class Chat(
|
data class Chat(
|
||||||
val id: String,
|
val id: String,
|
||||||
val type: String,
|
val type: String,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package chats.domain.model
|
package chats.domain.model
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
|
||||||
|
@Immutable
|
||||||
data class Message(
|
data class Message(
|
||||||
val id: String,
|
val id: String,
|
||||||
val chatId: String,
|
val chatId: String,
|
||||||
@@ -19,6 +22,7 @@ data class Message(
|
|||||||
val replyTo: Message? = null
|
val replyTo: Message? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
data class Media(
|
data class Media(
|
||||||
val id: String,
|
val id: String,
|
||||||
val type: String,
|
val type: String,
|
||||||
|
|||||||
@@ -152,39 +152,46 @@ fun ChatDetailScreen(
|
|||||||
val items = mutableListOf<MessageListItem>()
|
val items = mutableListOf<MessageListItem>()
|
||||||
if (state.messages.isEmpty()) return@remember items
|
if (state.messages.isEmpty()) return@remember items
|
||||||
|
|
||||||
// Группируем от новых к старым (Index 0 = bottom)
|
// Группируем от старых к новым (для normal layout)
|
||||||
|
val reversedMessages = state.messages.reversed()
|
||||||
var i = 0
|
var i = 0
|
||||||
while (i < state.messages.size) {
|
while (i < reversedMessages.size) {
|
||||||
val isoDate = state.messages[i].createdAt
|
val isoDate = reversedMessages[i].createdAt
|
||||||
// Получаем дату в локальном часовом поясе (например "2024-04-15")
|
|
||||||
val localDate = viewModel.getLocalDateString(isoDate)
|
val localDate = viewModel.getLocalDateString(isoDate)
|
||||||
val dayMessages = mutableListOf<chats.domain.model.Message>()
|
val dayMessages = mutableListOf<chats.domain.model.Message>()
|
||||||
|
|
||||||
// Собираем все сообщения за этот локальный день
|
while (i < reversedMessages.size && viewModel.getLocalDateString(reversedMessages[i].createdAt) == localDate) {
|
||||||
while (i < state.messages.size && viewModel.getLocalDateString(state.messages[i].createdAt) == localDate) {
|
dayMessages.add(reversedMessages[i])
|
||||||
dayMessages.add(state.messages[i])
|
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем сообщения в инвертированном порядке
|
// Добавляем заголовок даты
|
||||||
|
items.add(MessageListItem.DateHeader(viewModel.formatDateHeader(isoDate)))
|
||||||
|
|
||||||
|
// Добавляем сообщения
|
||||||
dayMessages.forEach { msg ->
|
dayMessages.forEach { msg ->
|
||||||
items.add(MessageListItem.MessageItem(msg))
|
items.add(MessageListItem.MessageItem(msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем заголовок (над группой сообщений)
|
|
||||||
items.add(MessageListItem.DateHeader(viewModel.formatDateHeader(isoDate)))
|
|
||||||
}
|
}
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Выносим allChatMedia наружу - до LazyColumn
|
||||||
|
val allChatMedia = remember(state.messages) {
|
||||||
|
state.messages.flatMap { msg -> msg.media.map { it to msg.id } }
|
||||||
|
}
|
||||||
|
|
||||||
// Отслеживаем текущую дату для плавающего заголовка
|
// Отслеживаем текущую дату для плавающего заголовка
|
||||||
val floatingDate by remember {
|
val floatingDate by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
val firstIndex = listState.firstVisibleItemIndex
|
val layoutInfo = listState.layoutInfo
|
||||||
// Не показываем плашку на самом первом элементе (там и так дата)
|
val visibleItems = layoutInfo.visibleItemsInfo
|
||||||
// И показываем только если мы прокрутили хотя бы немного
|
if (visibleItems.isEmpty()) return@derivedStateOf null
|
||||||
if (firstIndex > 0 && firstIndex < listItems.size) {
|
|
||||||
val item = listItems[firstIndex]
|
// Находим первый видимый элемент
|
||||||
|
val firstVisibleItemIndex = visibleItems.first().index
|
||||||
|
if (firstVisibleItemIndex > 0 && firstVisibleItemIndex < listItems.size) {
|
||||||
|
val item = listItems[firstVisibleItemIndex]
|
||||||
if (item is MessageListItem.MessageItem) {
|
if (item is MessageListItem.MessageItem) {
|
||||||
viewModel.formatDateHeader(item.message.createdAt)
|
viewModel.formatDateHeader(item.message.createdAt)
|
||||||
} else if (item is MessageListItem.DateHeader) {
|
} else if (item is MessageListItem.DateHeader) {
|
||||||
@@ -195,54 +202,56 @@ fun ChatDetailScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Показывать ли кнопку "вниз"
|
// Показывать ли кнопку "вниз"
|
||||||
// Она должна появляться если мы не в самом низу
|
|
||||||
// Проверка видимости кнопки скролла вниз
|
|
||||||
val showScrollToBottom by remember {
|
val showScrollToBottom by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
listState.firstVisibleItemIndex > 5
|
val layoutInfo = listState.layoutInfo
|
||||||
|
val visibleItems = layoutInfo.visibleItemsInfo
|
||||||
|
val totalCount = layoutInfo.totalItemsCount
|
||||||
|
if (totalCount == 0) return@derivedStateOf false
|
||||||
|
|
||||||
|
// Проверяем, видим ли мы последний элемент
|
||||||
|
val lastVisibleIndex = visibleItems.lastOrNull()?.index ?: 0
|
||||||
|
lastVisibleIndex < totalCount - 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Авто-скролл к новым сообщениям
|
// Авто-скролл к новым сообщениям - только если мы уже внизу
|
||||||
val firstMessageId = state.messages.firstOrNull()?.id
|
val lastMessageId = state.messages.lastOrNull()?.id
|
||||||
LaunchedEffect(firstMessageId) {
|
LaunchedEffect(lastMessageId) {
|
||||||
if (firstMessageId != null) {
|
if (lastMessageId != null && listItems.isNotEmpty()) {
|
||||||
// Если мы уже внизу (индекс 0) или это наше сообщение - скроллим вниз
|
val isAtBottom = listState.layoutInfo.run {
|
||||||
val isAtBottom = listState.firstVisibleItemIndex <= 1
|
val visibleItems = visibleItemsInfo
|
||||||
val isMyMessage = state.messages.firstOrNull()?.senderId == viewModel.getCurrentUserId()
|
val totalCount = totalItemsCount
|
||||||
|
if (totalCount == 0 || visibleItems.isEmpty()) return@run true
|
||||||
if (isAtBottom || isMyMessage) {
|
visibleItems.last().index >= totalCount - 2
|
||||||
listState.animateScrollToItem(0)
|
|
||||||
// Если мы внизу - помечаем сразу как прочитанное
|
|
||||||
if (isAtBottom && !isMyMessage) {
|
|
||||||
viewModel.markAsRead()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isAtBottom) {
|
||||||
|
listState.animateScrollToItem(listItems.size - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Автоматическая прокрутка и прочтение при инициализации и новых сообщениях
|
// Автоматическая прокрутка при инициализации
|
||||||
LaunchedEffect(listItems.size) {
|
LaunchedEffect(listItems.size) {
|
||||||
if (listItems.isNotEmpty()) {
|
if (listItems.isNotEmpty()) {
|
||||||
if (state.initialScrollIndex != null && state.initialScrollIndex != -1) {
|
listState.scrollToItem(listItems.size - 1)
|
||||||
// Первый вход в чат - мы и так внизу из-за reverseLayout=true, но можем форсированно прокрутить к 0
|
|
||||||
listState.scrollToItem(0)
|
|
||||||
viewModel.onInitialScrollDone()
|
viewModel.onInitialScrollDone()
|
||||||
viewModel.markAsRead()
|
viewModel.markAsRead()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Подгрузка истории при прокрутке вверх
|
// Подгрузка истории при прокрутке вверх
|
||||||
LaunchedEffect(listState, state.messages.size) {
|
LaunchedEffect(listState) {
|
||||||
snapshotFlow {
|
snapshotFlow { listState.layoutInfo }
|
||||||
val lastIndex = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
|
.collect { layoutInfo ->
|
||||||
val totalCount = listState.layoutInfo.totalItemsCount
|
val visibleItems = layoutInfo.visibleItemsInfo
|
||||||
lastIndex to totalCount
|
val totalCount = layoutInfo.totalItemsCount
|
||||||
}
|
if (totalCount == 0 || visibleItems.isEmpty()) return@collect
|
||||||
.collect { (lastVisibleIndex, totalCount) ->
|
|
||||||
if (totalCount > 0 && lastVisibleIndex >= totalCount - 20 && !state.isLoadingMore) {
|
val firstVisibleIndex = visibleItems.first().index
|
||||||
android.util.Log.d("ChatDetailScreen", "TRIGGER LOAD MORE: index $lastVisibleIndex, total $totalCount")
|
if (firstVisibleIndex < 5 && !state.isLoadingMore) {
|
||||||
|
android.util.Log.d("ChatDetailScreen", "TRIGGER LOAD MORE: index $firstVisibleIndex")
|
||||||
viewModel.loadMoreMessages()
|
viewModel.loadMoreMessages()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,12 +261,20 @@ fun ChatDetailScreen(
|
|||||||
state.messages.count { !it.isRead && it.senderId != viewModel.getCurrentUserId() }
|
state.messages.count { !it.isRead && it.senderId != viewModel.getCurrentUserId() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Авто-прочитка при нахождении внизу списка (в reverseLayout это индекс 0)
|
// Авто-прочитка при нахождении внизу списка
|
||||||
LaunchedEffect(listState.isScrollInProgress, listState.firstVisibleItemIndex) {
|
LaunchedEffect(listState.isScrollInProgress) {
|
||||||
if (!listState.isScrollInProgress && listState.firstVisibleItemIndex <= 1) {
|
if (!listState.isScrollInProgress) {
|
||||||
|
val layoutInfo = listState.layoutInfo
|
||||||
|
val visibleItems = layoutInfo.visibleItemsInfo
|
||||||
|
val totalCount = layoutInfo.totalItemsCount
|
||||||
|
if (totalCount > 0 && visibleItems.isNotEmpty()) {
|
||||||
|
val lastVisibleIndex = visibleItems.last().index
|
||||||
|
if (lastVisibleIndex >= totalCount - 2) {
|
||||||
viewModel.markAsRead()
|
viewModel.markAsRead()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -338,21 +355,26 @@ fun ChatDetailScreen(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
reverseLayout = true,
|
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
val allChatMedia = state.messages.flatMap { msg ->
|
items(
|
||||||
msg.media.map { it to msg.id }
|
items = listItems,
|
||||||
}.reversed()
|
key = { item ->
|
||||||
|
when(item) {
|
||||||
items(listItems, key = {
|
is MessageListItem.MessageItem -> "msg_${item.message.id}"
|
||||||
when(it) {
|
is MessageListItem.DateHeader -> "date_${item.date}"
|
||||||
is MessageListItem.MessageItem -> it.message.id
|
|
||||||
is MessageListItem.DateHeader -> "date_${it.date}"
|
|
||||||
MessageListItem.UnreadSeparator -> "unread_separator"
|
MessageListItem.UnreadSeparator -> "unread_separator"
|
||||||
}
|
}
|
||||||
}) { item ->
|
},
|
||||||
|
contentType = { item ->
|
||||||
|
when(item) {
|
||||||
|
is MessageListItem.MessageItem -> "message"
|
||||||
|
is MessageListItem.DateHeader -> "date_header"
|
||||||
|
MessageListItem.UnreadSeparator -> "unread_separator"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { item ->
|
||||||
when(item) {
|
when(item) {
|
||||||
is MessageListItem.MessageItem -> {
|
is MessageListItem.MessageItem -> {
|
||||||
SwipeableMessageItem(
|
SwipeableMessageItem(
|
||||||
@@ -487,7 +509,7 @@ fun ChatDetailScreen(
|
|||||||
.border(0.5.dp, Color.White.copy(alpha = 0.3f), CircleShape)
|
.border(0.5.dp, Color.White.copy(alpha = 0.3f), CircleShape)
|
||||||
.clickable {
|
.clickable {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
listState.animateScrollToItem(0)
|
listState.animateScrollToItem(listItems.size - 1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
@@ -1032,6 +1054,7 @@ fun ForwardChatSelectionDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
sealed class MessageListItem {
|
sealed class MessageListItem {
|
||||||
data class MessageItem(val message: chats.domain.model.Message) : MessageListItem()
|
data class MessageItem(val message: chats.domain.model.Message) : MessageListItem()
|
||||||
data class DateHeader(val date: String) : MessageListItem()
|
data class DateHeader(val date: String) : MessageListItem()
|
||||||
|
|||||||
@@ -200,7 +200,9 @@ private fun EmojiContent(
|
|||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Fixed(8),
|
columns = GridCells.Fixed(8),
|
||||||
state = scrollState,
|
state = scrollState,
|
||||||
modifier = Modifier.weight(1f).padding(horizontal = 4.dp)
|
modifier = Modifier.weight(1f).padding(horizontal = 4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
emojiCategories.forEach { category ->
|
emojiCategories.forEach { category ->
|
||||||
// Заголовок категории
|
// Заголовок категории
|
||||||
@@ -214,7 +216,7 @@ private fun EmojiContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
items(category.emojis) { emoji ->
|
items(category.emojis, key = { emoji -> emoji }) { emoji ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.aspectRatio(1f)
|
.aspectRatio(1f)
|
||||||
@@ -362,9 +364,11 @@ private fun GifContent(
|
|||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Fixed(3),
|
columns = GridCells.Fixed(3),
|
||||||
modifier = Modifier.weight(1f).padding(4.dp),
|
modifier = Modifier.weight(1f).padding(4.dp),
|
||||||
contentPadding = PaddingValues(4.dp)
|
contentPadding = PaddingValues(4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
items(displayGifs) { gif ->
|
items(displayGifs, key = { gif -> gif.id }, contentType = { "gif" }) { gif ->
|
||||||
val url = gif.files?.get("hd")?.get("gif")?.url
|
val url = gif.files?.get("hd")?.get("gif")?.url
|
||||||
?: gif.files?.get("sd")?.get("gif")?.url
|
?: gif.files?.get("sd")?.get("gif")?.url
|
||||||
?: gif.file?.get("hd")?.get("gif")?.url
|
?: gif.file?.get("hd")?.get("gif")?.url
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package chats.presentation.components
|
package chats.presentation.components
|
||||||
|
|
||||||
import androidx.compose.animation.core.Animatable
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
import androidx.compose.foundation.gestures.draggable
|
import androidx.compose.foundation.gestures.draggable
|
||||||
@@ -30,10 +29,10 @@ fun SwipeableMessageItem(
|
|||||||
) {
|
) {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val offsetX = remember { Animatable(0f) }
|
|
||||||
val replyThreshold = with(density) { 60.dp.toPx() }
|
val replyThreshold = with(density) { 60.dp.toPx() }
|
||||||
val maxDrag = with(density) { 90.dp.toPx() }
|
val maxDrag = with(density) { 90.dp.toPx() }
|
||||||
|
|
||||||
|
var offsetX by remember { mutableFloatStateOf(0f) }
|
||||||
var isTriggered by remember { mutableStateOf(false) }
|
var isTriggered by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
@@ -42,28 +41,19 @@ fun SwipeableMessageItem(
|
|||||||
.draggable(
|
.draggable(
|
||||||
orientation = Orientation.Horizontal,
|
orientation = Orientation.Horizontal,
|
||||||
state = rememberDraggableState { delta ->
|
state = rememberDraggableState { delta ->
|
||||||
scope.launch {
|
|
||||||
// We only allow swiping to the left (negative delta)
|
// We only allow swiping to the left (negative delta)
|
||||||
val newOffset = (offsetX.value + delta).coerceIn(-maxDrag, 0f)
|
val newOffset = (offsetX + delta).coerceIn(-maxDrag, 0f)
|
||||||
offsetX.snapTo(newOffset)
|
offsetX = newOffset
|
||||||
|
|
||||||
if (newOffset <= -replyThreshold && !isTriggered) {
|
isTriggered = newOffset <= -replyThreshold
|
||||||
isTriggered = true
|
|
||||||
// Trigger haptic feedback if available?
|
|
||||||
} else if (newOffset > -replyThreshold) {
|
|
||||||
isTriggered = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onDragStopped = {
|
onDragStopped = {
|
||||||
if (offsetX.value <= -replyThreshold) {
|
if (offsetX <= -replyThreshold) {
|
||||||
onReply()
|
onReply()
|
||||||
}
|
}
|
||||||
scope.launch {
|
offsetX = 0f
|
||||||
offsetX.animateTo(0f)
|
|
||||||
isTriggered = false
|
isTriggered = false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Reply Icon (Underneath)
|
// Reply Icon (Underneath)
|
||||||
@@ -72,7 +62,7 @@ fun SwipeableMessageItem(
|
|||||||
.align(Alignment.CenterEnd)
|
.align(Alignment.CenterEnd)
|
||||||
.padding(end = 16.dp)
|
.padding(end = 16.dp)
|
||||||
.size(36.dp)
|
.size(36.dp)
|
||||||
.alpha(((-offsetX.value) / replyThreshold).coerceIn(0f, 1f))
|
.alpha(((-offsetX) / replyThreshold).coerceIn(0f, 1f))
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(if (isTriggered) MaterialTheme.colorScheme.primary else Color.Gray.copy(alpha = 0.2f)),
|
.background(if (isTriggered) MaterialTheme.colorScheme.primary else Color.Gray.copy(alpha = 0.2f)),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
@@ -88,7 +78,7 @@ fun SwipeableMessageItem(
|
|||||||
// Message Content
|
// Message Content
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
|
.offset { IntOffset(offsetX.roundToInt(), 0) }
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
|
|||||||
+1
-8
@@ -1,12 +1,5 @@
|
|||||||
package core.notifications.data
|
package core.notifications.data
|
||||||
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import com.google.firebase.messaging.FirebaseMessagingService
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
import com.google.firebase.messaging.RemoteMessage
|
import com.google.firebase.messaging.RemoteMessage
|
||||||
import auth.data.remote.api.AuthApi
|
import auth.data.remote.api.AuthApi
|
||||||
@@ -18,7 +11,7 @@ import kotlinx.coroutines.launch
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ForkFirebaseMessagingService : FirebaseMessagingService() {
|
class KnotFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var authApi: AuthApi
|
lateinit var authApi: AuthApi
|
||||||
@@ -7,9 +7,10 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
import ru.knot.messager.R
|
||||||
|
|
||||||
object NotificationHelper {
|
object NotificationHelper {
|
||||||
private const val CHANNEL_ID = "fork_notifications_channel"
|
private const val CHANNEL_ID = "knot_notifications_channel"
|
||||||
|
|
||||||
fun showNotification(context: Context, title: String?, body: String?, type: String?, chatId: String? = null, notificationId: Int? = null, totalCount: Int = 0) {
|
fun showNotification(context: Context, title: String?, body: String?, type: String?, chatId: String? = null, notificationId: Int? = null, totalCount: Int = 0) {
|
||||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
@@ -19,7 +20,7 @@ object NotificationHelper {
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val channel = NotificationChannel(
|
val channel = NotificationChannel(
|
||||||
CHANNEL_ID,
|
CHANNEL_ID,
|
||||||
"Fork Messenger Notifications",
|
"Knot Notifications",
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
).apply {
|
).apply {
|
||||||
description = "Chat and system notifications"
|
description = "Chat and system notifications"
|
||||||
@@ -42,7 +43,7 @@ object NotificationHelper {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_ID)
|
val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
.setContentText(body)
|
.setContentText(body)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ private val DarkColors = darkColorScheme(
|
|||||||
val SoftSquareShape = RoundedCornerShape(14.dp) // "Мягкий квадрат" как в вэб
|
val SoftSquareShape = RoundedCornerShape(14.dp) // "Мягкий квадрат" как в вэб
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ForkMessengerTheme(content: @Composable () -> Unit) {
|
fun KnotTheme(content: @Composable () -> Unit) {
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = DarkColors,
|
colorScheme = DarkColors,
|
||||||
shapes = MaterialTheme.shapes.copy(
|
shapes = MaterialTheme.shapes.copy(
|
||||||
|
|||||||
@@ -284,8 +284,8 @@ fun LazyListScope.renderFiles() {
|
|||||||
fun LazyListScope.renderLinks() {
|
fun LazyListScope.renderLinks() {
|
||||||
items(2) {
|
items(2) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text("https://github.com/forkmessager") },
|
headlineContent = { Text("https://github.com/knot-messenger") },
|
||||||
supportingContent = { Text("GitHub - ForkMessager Project") },
|
supportingContent = { Text("GitHub - Knot Project") },
|
||||||
leadingContent = { Icon(Icons.Default.Link, contentDescription = null) }
|
leadingContent = { Icon(Icons.Default.Link, contentDescription = null) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user