diff --git a/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt b/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt index a0a318a2..53b5fc1a 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt @@ -144,7 +144,8 @@ fun RootScreen( messagesHistoryScreen( onError = viewModel::onError, onBack = navController::navigateUp, - onNavigateToChatMaterials = navController::navigateToChatMaterials + onNavigateToChatMaterials = navController::navigateToChatMaterials, + onNavigateToPhotoViewer = navController::navigateToPhotoView ) chatMaterialsScreen( onBack = navController::navigateUp, diff --git a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/AppSettings.kt b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/AppSettings.kt index 386f2365..b48335a3 100644 --- a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/AppSettings.kt +++ b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/AppSettings.kt @@ -96,6 +96,13 @@ object AppSettings { ) set(value) = put(SettingsKeys.KEY_SHOW_EMOJI_BUTTON, value) + var showAttachmentButton: Boolean + get() = get( + SettingsKeys.KEY_SHOW_ATTACHMENT_BUTTON, + SettingsKeys.DEFAULT_VALUE_SHOW_ATTACHMENT_BUTTON + ) + set(value) = put(SettingsKeys.KEY_SHOW_ATTACHMENT_BUTTON, value) + var enableHaptic: Boolean get() = get( SettingsKeys.KEY_ENABLE_HAPTIC, diff --git a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt index 8f69cdf8..30a2b7a8 100644 --- a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt +++ b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt @@ -11,6 +11,10 @@ object SettingsKeys { const val DEFAULT_VALUE_USE_CONTACT_NAMES = false const val KEY_SHOW_EMOJI_BUTTON = "show_emoji_button" const val DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON = false + const val KEY_SHOW_ATTACHMENT_BUTTON = "show_attachment_button" + const val DEFAULT_VALUE_SHOW_ATTACHMENT_BUTTON = false + const val KEY_SHOW_RECORD_VOICE_BUTTON = "show_record_voice_button" + const val DEFAULT_VALUE_SHOW_RECORD_VOICE_BUTTON = false const val KEY_APPEARANCE = "appearance" const val KEY_APPEARANCE_MULTILINE = "appearance_multiline" diff --git a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/UserSettings.kt b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/UserSettings.kt index 080b4aa9..7c092062 100644 --- a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/UserSettings.kt +++ b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/UserSettings.kt @@ -14,15 +14,10 @@ interface UserSettings { val enableDynamicColors: StateFlow val appLanguage: StateFlow - val fastText: StateFlow - val sendOnlineStatus: StateFlow - val showAlertAfterCrash: StateFlow val longPollInBackground: StateFlow val useBlur: StateFlow - val showEmojiButton: StateFlow - val showTimeInActionMessages: StateFlow val useSystemFont: StateFlow val enableAnimations: StateFlow val showDebugCategory: StateFlow @@ -35,15 +30,10 @@ interface UserSettings { fun onEnableDynamicColorsChanged(enable: Boolean) fun onAppLanguageChanged(language: String) - fun onFastTextChanged(text: String) - fun onSendOnlineStatusChanged(send: Boolean) - fun onShowAlertAfterCrashChanged(show: Boolean) fun onLongPollInBackgroundChanged(inBackground: Boolean) fun onUseBlurChanged(use: Boolean) - fun onShowEmojiButtonChanged(show: Boolean) - fun onShowTimeInActionMessagesChanged(show: Boolean) fun onUseSystemFontChanged(use: Boolean) fun onShowDebugCategoryChanged(show: Boolean) } @@ -58,16 +48,11 @@ class UserSettingsImpl : UserSettings { override val enableDynamicColors = MutableStateFlow(AppSettings.Appearance.enableDynamicColors) override val appLanguage = MutableStateFlow(AppSettings.Appearance.appLanguage) - override val fastText = MutableStateFlow(AppSettings.Features.fastText) - override val sendOnlineStatus = MutableStateFlow(AppSettings.Activity.sendOnlineStatus) - override val showAlertAfterCrash = MutableStateFlow(AppSettings.Debug.showAlertAfterCrash) - override val longPollInBackground = MutableStateFlow(AppSettings.Experimental.longPollInBackground) + override val longPollInBackground = + MutableStateFlow(AppSettings.Experimental.longPollInBackground) override val useBlur = MutableStateFlow(AppSettings.Experimental.useBlur) - override val showEmojiButton = MutableStateFlow(AppSettings.General.showEmojiButton) - override val showTimeInActionMessages = - MutableStateFlow(AppSettings.Experimental.showTimeInActionMessages) override val useSystemFont = MutableStateFlow(AppSettings.Appearance.useSystemFont) override val enableAnimations = MutableStateFlow(AppSettings.Experimental.moreAnimations) override val showDebugCategory = MutableStateFlow(AppSettings.Debug.showDebugCategory) @@ -96,18 +81,10 @@ class UserSettingsImpl : UserSettings { appLanguage.value = language } - override fun onFastTextChanged(text: String) { - fastText.value = text - } - override fun onSendOnlineStatusChanged(send: Boolean) { sendOnlineStatus.value = send } - override fun onShowAlertAfterCrashChanged(show: Boolean) { - showAlertAfterCrash.value = show - } - override fun onLongPollInBackgroundChanged(inBackground: Boolean) { longPollInBackground.value = inBackground } @@ -116,14 +93,6 @@ class UserSettingsImpl : UserSettings { useBlur.value = use } - override fun onShowEmojiButtonChanged(show: Boolean) { - showEmojiButton.value = show - } - - override fun onShowTimeInActionMessagesChanged(show: Boolean) { - showTimeInActionMessages.value = show - } - override fun onUseSystemFontChanged(use: Boolean) { useSystemFont.value = use } diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/FullScreenLoader.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/Loaders.kt similarity index 50% rename from core/ui/src/main/kotlin/dev/meloda/fast/ui/components/FullScreenLoader.kt rename to core/ui/src/main/kotlin/dev/meloda/fast/ui/components/Loaders.kt index b8b738f6..c79724b5 100644 --- a/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/FullScreenLoader.kt +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/Loaders.kt @@ -5,13 +5,27 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.material3.ContainedLoadingIndicator import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +@Preview +fun FullScreenContainedLoader(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding(), + contentAlignment = Alignment.Center + ) { + ContainedLoader() + } +} + +@Preview @Composable fun FullScreenLoader(modifier: Modifier = Modifier) { Box( @@ -20,15 +34,28 @@ fun FullScreenLoader(modifier: Modifier = Modifier) { .navigationBarsPadding(), contentAlignment = Alignment.Center ) { - ContainedLoadingIndicator( - containerColor = MaterialTheme.colorScheme.primary, - indicatorColor = MaterialTheme.colorScheme.primaryContainer - ) + Loader() } } -@Preview +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -private fun FullScreenLoaderPreview() { - FullScreenLoader() +@Preview +fun ContainedLoader(modifier: Modifier = Modifier) { + ContainedLoadingIndicator( + modifier = modifier, + containerColor = MaterialTheme.colorScheme.primary, + indicatorColor = MaterialTheme.colorScheme.primaryContainer + ) } + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +@Preview +fun Loader(modifier: Modifier = Modifier) { + LoadingIndicator( + modifier = modifier, + color = MaterialTheme.colorScheme.primary + ) +} + diff --git a/core/ui/src/main/res/values-ru/strings.xml b/core/ui/src/main/res/values-ru/strings.xml index 458b610d..f091a6bb 100644 --- a/core/ui/src/main/res/values-ru/strings.xml +++ b/core/ui/src/main/res/values-ru/strings.xml @@ -194,6 +194,7 @@ Основное Использовать имена контактов Приложение будет использовать доступные имена контактов для пользователей + Показывать кнопку вложений на панели чата Включить тактильную отдачу Внешний вид Многострочные заголовки и сообщения @@ -272,4 +273,8 @@ Обычный Регистрация Забыли пароль? + Показывать кнопку вложений + Скопировать ссылку + Скопировать + Скопировать изображение diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 6bc4b50a..2403f0ac 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -260,6 +260,8 @@ App will use available contact names for users Show emoji button Show emoji button in chat panel + Show attachment button + Show attachment button in chat panel Enable haptic Appearance Multiline titles and messages @@ -348,4 +350,8 @@ Regular Sign up Forgot password? + + Copy link + Copy + Copy image diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/AudioMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/AudioMaterialsScreen.kt index 74155846..72f19aae 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/AudioMaterialsScreen.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/AudioMaterialsScreen.kt @@ -53,7 +53,7 @@ import dev.meloda.fast.model.BaseError import dev.meloda.fast.ui.R import dev.meloda.fast.ui.basic.ContentAlpha import dev.meloda.fast.ui.basic.LocalContentAlpha -import dev.meloda.fast.ui.components.FullScreenLoader +import dev.meloda.fast.ui.components.FullScreenContainedLoader import dev.meloda.fast.ui.components.NoItemsView import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.theme.LocalHazeState @@ -105,7 +105,7 @@ fun AudioMaterialsScreen( VkErrorView(baseError = baseError) } - screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader() + screenState.isLoading && screenState.materials.isEmpty() -> FullScreenContainedLoader() else -> { PullToRefreshBox( diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/FileMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/FileMaterialsScreen.kt index 1d477c23..8a3ed3c4 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/FileMaterialsScreen.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/FileMaterialsScreen.kt @@ -63,7 +63,7 @@ import dev.meloda.fast.model.BaseError import dev.meloda.fast.ui.R import dev.meloda.fast.ui.basic.ContentAlpha import dev.meloda.fast.ui.basic.LocalContentAlpha -import dev.meloda.fast.ui.components.FullScreenLoader +import dev.meloda.fast.ui.components.FullScreenContainedLoader import dev.meloda.fast.ui.components.NoItemsView import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.theme.LocalHazeState @@ -114,7 +114,7 @@ fun FileMaterialsScreen( VkErrorView(baseError = baseError) } - screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader() + screenState.isLoading && screenState.materials.isEmpty() -> FullScreenContainedLoader() else -> { PullToRefreshBox( diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/LinkMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/LinkMaterialsScreen.kt index 05e193e1..332b561f 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/LinkMaterialsScreen.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/LinkMaterialsScreen.kt @@ -63,7 +63,7 @@ import dev.meloda.fast.model.BaseError import dev.meloda.fast.ui.R import dev.meloda.fast.ui.basic.ContentAlpha import dev.meloda.fast.ui.basic.LocalContentAlpha -import dev.meloda.fast.ui.components.FullScreenLoader +import dev.meloda.fast.ui.components.FullScreenContainedLoader import dev.meloda.fast.ui.components.NoItemsView import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.theme.LocalHazeState @@ -114,7 +114,7 @@ fun LinkMaterialsScreen( VkErrorView(baseError = baseError) } - screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader() + screenState.isLoading && screenState.materials.isEmpty() -> FullScreenContainedLoader() else -> { PullToRefreshBox( diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/PhotoMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/PhotoMaterialsScreen.kt index db04ffb5..cae82e01 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/PhotoMaterialsScreen.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/PhotoMaterialsScreen.kt @@ -46,7 +46,7 @@ import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState import dev.meloda.fast.chatmaterials.model.UiChatMaterial import dev.meloda.fast.model.BaseError import dev.meloda.fast.ui.R -import dev.meloda.fast.ui.components.FullScreenLoader +import dev.meloda.fast.ui.components.FullScreenContainedLoader import dev.meloda.fast.ui.components.NoItemsView import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.theme.LocalHazeState @@ -98,7 +98,7 @@ fun PhotoMaterialsScreen( VkErrorView(baseError = baseError) } - screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader() + screenState.isLoading && screenState.materials.isEmpty() -> FullScreenContainedLoader() else -> { PullToRefreshBox( diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/VideoMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/VideoMaterialsScreen.kt index acd85816..e849eb69 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/VideoMaterialsScreen.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/materials/VideoMaterialsScreen.kt @@ -56,7 +56,7 @@ import dev.meloda.fast.model.BaseError import dev.meloda.fast.ui.R import dev.meloda.fast.ui.basic.ContentAlpha import dev.meloda.fast.ui.basic.LocalContentAlpha -import dev.meloda.fast.ui.components.FullScreenLoader +import dev.meloda.fast.ui.components.FullScreenContainedLoader import dev.meloda.fast.ui.components.NoItemsView import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.theme.LocalHazeState @@ -107,7 +107,7 @@ fun VideoMaterialsScreen( VkErrorView(baseError = baseError) } - screenState.isLoading && screenState.materials.isEmpty() -> FullScreenLoader() + screenState.isLoading && screenState.materials.isEmpty() -> FullScreenContainedLoader() else -> { PullToRefreshBox( diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt index ed63f6d3..ee20f615 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt @@ -44,7 +44,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -65,7 +64,7 @@ import dev.chrisbanes.haze.materials.HazeMaterials import dev.meloda.fast.conversations.model.ConversationsScreenState import dev.meloda.fast.conversations.navigation.ConversationsGraph import dev.meloda.fast.model.BaseError -import dev.meloda.fast.ui.components.FullScreenLoader +import dev.meloda.fast.ui.components.FullScreenContainedLoader import dev.meloda.fast.ui.components.NoItemsView import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.model.api.ConversationOption @@ -306,7 +305,7 @@ fun ConversationsScreen( ) } - screenState.isLoading && conversations.isEmpty() -> FullScreenLoader() + screenState.isLoading && conversations.isEmpty() -> FullScreenContainedLoader() else -> { val pullToRefreshState = rememberPullToRefreshState() diff --git a/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/presentation/CreateChatScreen.kt b/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/presentation/CreateChatScreen.kt index 7b1b9cc7..cac5f769 100644 --- a/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/presentation/CreateChatScreen.kt +++ b/feature/createchat/src/main/kotlin/dev/meloda/fast/conversations/presentation/CreateChatScreen.kt @@ -51,7 +51,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.LayoutDirection @@ -64,8 +63,7 @@ import dev.chrisbanes.haze.materials.HazeMaterials import dev.meloda.fast.conversations.CreateChatViewModel import dev.meloda.fast.conversations.model.CreateChatScreenState 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.FullScreenContainedLoader import dev.meloda.fast.ui.components.IconButton import dev.meloda.fast.ui.components.NoItemsView import dev.meloda.fast.ui.components.VkErrorView @@ -271,7 +269,7 @@ fun CreateChatScreen( VkErrorView(baseError = baseError) } - screenState.isLoading && screenState.friends.isEmpty() -> FullScreenLoader() + screenState.isLoading && screenState.friends.isEmpty() -> FullScreenContainedLoader() else -> { val pullToRefreshState = rememberPullToRefreshState() diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt index 3055d765..92a126ea 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/FriendsScreen.kt @@ -1,7 +1,6 @@ package dev.meloda.fast.friends.presentation import android.content.Context -import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding @@ -28,11 +27,9 @@ import coil.imageLoader import coil.request.ImageRequest import dev.chrisbanes.haze.hazeSource import dev.meloda.fast.friends.FriendsViewModel -import dev.meloda.fast.friends.FriendsViewModelImpl -import dev.meloda.fast.friends.OnlineFriendsViewModelImpl import dev.meloda.fast.friends.navigation.Friends import dev.meloda.fast.ui.R -import dev.meloda.fast.ui.components.FullScreenLoader +import dev.meloda.fast.ui.components.FullScreenContainedLoader import dev.meloda.fast.ui.components.NoItemsView import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.theme.LocalHazeState @@ -41,7 +38,6 @@ import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.util.ImmutableList import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce -import org.koin.androidx.compose.koinViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -136,7 +132,7 @@ fun FriendsScreen( } when { - screenState.isLoading && screenState.friends.isEmpty() -> FullScreenLoader() + screenState.isLoading && screenState.friends.isEmpty() -> FullScreenContainedLoader() else -> { val pullToRefreshState = rememberPullToRefreshState() diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt index 00e07e28..67d55474 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt @@ -3,6 +3,7 @@ package dev.meloda.fast.messageshistory import android.content.ClipData import android.content.ClipboardManager import android.content.Context +import android.graphics.Bitmap import android.os.Build import android.os.Bundle import android.util.Log @@ -15,9 +16,13 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextDecoration +import androidx.core.content.FileProvider +import androidx.core.graphics.drawable.toBitmapOrNull import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import coil.imageLoader +import coil.request.ImageRequest import com.conena.nanokt.collections.indexOfFirstOrNull import com.conena.nanokt.text.isEmptyOrBlank import com.conena.nanokt.text.isNotEmptyOrBlank @@ -52,12 +57,16 @@ import dev.meloda.fast.model.LongPollParsedEvent import dev.meloda.fast.model.api.domain.FormatDataType import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkMessage +import dev.meloda.fast.model.api.domain.VkPhotoDomain import dev.meloda.fast.network.VkErrorCode import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream import kotlin.math.abs import kotlin.random.Random import dev.meloda.fast.ui.R as UiR @@ -163,10 +172,6 @@ class MessagesHistoryViewModelImpl( updatesParser.onMessageMarkedAsImportant(::handleMessageMarkedAsImportant) updatesParser.onMessageMarkedAsSpam(::handleMessageMarkedAsSpam) updatesParser.onMessageMarkedAsNotSpam(::handleMessageMarkedAsNotSpam) - - userSettings.showTimeInActionMessages.listenValue(viewModelScope) { - syncUiMessages() - } } override fun onNavigationConsumed() { @@ -1131,13 +1136,54 @@ class MessagesHistoryViewModelImpl( } private fun copyMessage(message: VkMessage) { - val contentToCopy = message.text.orEmpty().trim() - if (contentToCopy.isEmpty()) return - val clipboardManager = applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - clipboardManager.setPrimaryClip(ClipData.newPlainText("Message", contentToCopy)) + val messageToCopy = message.text.orEmpty().trim() + if (messageToCopy.isEmpty()) { + val photo = with(message.attachments.orEmpty()) { + if (size == 1 && all { it is VkPhotoDomain }) { + first() as? VkPhotoDomain + } else null + } ?: return + + val photoMaxSize = photo.getMaxSize() ?: return + + viewModelScope.launch(Dispatchers.IO) { + val drawable = applicationContext.imageLoader.execute( + ImageRequest.Builder(applicationContext) + .data(photoMaxSize.url) + .build() + ).drawable ?: return@launch + + val imagesDir = File(applicationContext.cacheDir, "images") + if (!imagesDir.exists()) imagesDir.mkdirs() + val imageFile = File(imagesDir, "shared_image_id${photo.id}.png") + FileOutputStream(imageFile).use { + drawable.toBitmapOrNull()?.compress(Bitmap.CompressFormat.PNG, 100, it) + } + + val uri = FileProvider.getUriForFile( + applicationContext, + "${applicationContext.packageName}.provider", + imageFile + ) + + val clip = ClipData.newUri(applicationContext.contentResolver, "Image", uri) + clipboardManager.setPrimaryClip(clip) + + withContext(Dispatchers.Main) { + Toast.makeText( + applicationContext, + "Image copied to clipboard", + Toast.LENGTH_SHORT + ).show() + } + } + return + } + + clipboardManager.setPrimaryClip(ClipData.newPlainText("Message", messageToCopy)) if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) { Toast.makeText(applicationContext, UiR.string.copied_to_clipboard, Toast.LENGTH_SHORT) @@ -1155,7 +1201,7 @@ class MessagesHistoryViewModelImpl( showName = false, prevMessage = messages.getOrNull(index + 1), nextMessage = messages.getOrNull(index - 1), - showTimeInActionMessages = userSettings.showTimeInActionMessages.value, + showTimeInActionMessages = AppSettings.Experimental.showTimeInActionMessages, conversation = screenState.value.conversation, isSelected = selectedMessages.indexOfFirstOrNull { it.id == message.id } != null ) diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/navigation/MessagesHistoryNavigation.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/navigation/MessagesHistoryNavigation.kt index 40c4c273..e5192f55 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/navigation/MessagesHistoryNavigation.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/navigation/MessagesHistoryNavigation.kt @@ -5,6 +5,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute +import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.messageshistory.model.MessagesHistoryArguments import dev.meloda.fast.messageshistory.presentation.MessagesHistoryRoute import dev.meloda.fast.model.BaseError @@ -27,13 +28,15 @@ data class MessagesHistory(val arguments: MessagesHistoryArguments) { fun NavGraphBuilder.messagesHistoryScreen( onError: (BaseError) -> Unit, onBack: () -> Unit, - onNavigateToChatMaterials: (peerId: Long, cmId: Long) -> Unit + onNavigateToChatMaterials: (peerId: Long, cmId: Long) -> Unit, + onNavigateToPhotoViewer: (images: List, index: Int) -> Unit, ) { composable(typeMap = MessagesHistory.typeMap) { MessagesHistoryRoute( onError = onError, onBack = onBack, - onNavigateToChatMaterials = onNavigateToChatMaterials + onNavigateToChatMaterials = onNavigateToChatMaterials, + onNavigateToPhotoViewer = onNavigateToPhotoViewer ) } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt index 57ebe097..09c720be 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/IncomingMessageBubble.kt @@ -16,6 +16,8 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -38,6 +40,9 @@ fun IncomingMessageBubble( onClick: (VkAttachment) -> Unit = {}, onLongClick: (VkAttachment) -> Unit = {} ) { + val currentOnClick by rememberUpdatedState(onClick) + val currentOnLongClick by rememberUpdatedState(onLongClick) + Row( modifier = modifier .fillMaxWidth() @@ -98,8 +103,8 @@ fun IncomingMessageBubble( isImportant = message.isImportant, isSelected = message.isSelected, attachments = message.attachments?.toImmutableList(), - onClick = onClick, - onLongClick = onLongClick + onClick = currentOnClick, + onLongClick = currentOnLongClick ) } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt index ebe5f4bc..bba9f51c 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -50,6 +51,9 @@ fun MessageBubble( onClick: (VkAttachment) -> Unit = {}, onLongClick: (VkAttachment) -> Unit = {} ) { + val currentOnClick by rememberUpdatedState(onClick) + val currentOnLongClick by rememberUpdatedState(onLongClick) + val theme = LocalThemeConfig.current val backgroundColor = if (!isOut) { MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) @@ -173,8 +177,8 @@ fun MessageBubble( Attachments( modifier = Modifier, attachments = attachments, - onClick = onClick, - onLongClick = onLongClick + onClick = currentOnClick, + onLongClick = currentOnLongClick ) val dateStatusBackground = if (theme.darkMode) Color.Black.copy(alpha = 0.5f) diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryInputBar.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryInputBar.kt index 4e2c03eb..11c0d30a 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryInputBar.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryInputBar.kt @@ -63,7 +63,9 @@ fun MessagesHistoryInputBar( modifier: Modifier = Modifier, message: TextFieldValue, hazeState: HazeState, + enableHaptic: Boolean, showEmojiButton: Boolean, + showAttachmentButton: Boolean, actionMode: ActionMode, onMessageInputChanged: (TextFieldValue) -> Unit = {}, onBoldRequested: () -> Unit = {}, @@ -179,7 +181,7 @@ fun MessagesHistoryInputBar( } TextField( - modifier = modifier + modifier = Modifier .weight(1f) .addTextContextMenuComponents { separator() @@ -236,46 +238,47 @@ fun MessagesHistoryInputBar( maxLines = 1, overflow = TextOverflow.Ellipsis ) - } + }, ) + if (showAttachmentButton) { + val attachmentRotation = remember { Animatable(0f) } - 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) + Column(verticalArrangement = Arrangement.Bottom) { + IconButton( + onClick = { + onAttachmentButtonClicked() + if (enableHaptic) { + view.performHapticFeedback( + HapticFeedbackConstantsCompat.REJECT ) - if (i > 0) { + } + scope.launch { + for (i in 20 downTo 0 step 4) { attachmentRotation.animateTo( - targetValue = -i.toFloat(), + 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) + ) } - ) { - 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)) + Spacer(modifier = Modifier.height(4.dp)) + } } val micRotation = remember { Animatable(0f) } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryRoute.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryRoute.kt index a29786e6..4fb61ed6 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryRoute.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryRoute.kt @@ -4,20 +4,21 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle -import dev.meloda.fast.datastore.UserSettings +import dev.meloda.fast.common.model.UiImage +import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.messageshistory.MessagesHistoryViewModel import dev.meloda.fast.messageshistory.MessagesHistoryViewModelImpl import dev.meloda.fast.messageshistory.model.MessageNavigation import dev.meloda.fast.model.BaseError import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList import org.koin.androidx.compose.koinViewModel -import org.koin.compose.koinInject @Composable fun MessagesHistoryRoute( onError: (BaseError) -> Unit, onBack: () -> Unit, onNavigateToChatMaterials: (peerId: Long, conversationMessageId: Long) -> Unit, + onNavigateToPhotoViewer: (images: List, index: Int) -> Unit, viewModel: MessagesHistoryViewModel = koinViewModel() ) { val screenState by viewModel.screenState.collectAsStateWithLifecycle() @@ -30,9 +31,6 @@ fun MessagesHistoryRoute( val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() val scrollIndex by viewModel.isNeedToScrollToIndex.collectAsStateWithLifecycle() - val userSettings: UserSettings = koinInject() - val showEmojiButton by userSettings.showEmojiButton.collectAsStateWithLifecycle() - LaunchedEffect(navigationEvent) { val needToConsume = when (val navigation = navigationEvent) { null -> false @@ -55,7 +53,9 @@ fun MessagesHistoryRoute( selectedMessages = selectedMessages.toImmutableList(), baseError = baseError, canPaginate = canPaginate, - showEmojiButton = showEmojiButton, + showEmojiButton = AppSettings.General.showEmojiButton, + showAttachmentButton = AppSettings.General.showAttachmentButton, + enableHaptic = AppSettings.General.enableHaptic, onBack = onBack, onClose = viewModel::onCloseButtonClicked, onScrolledToIndex = viewModel::onScrolledToIndex, @@ -69,6 +69,7 @@ fun MessagesHistoryRoute( onEmojiButtonLongClicked = viewModel::onEmojiButtonLongClicked, onMessageClicked = viewModel::onMessageClicked, onMessageLongClicked = viewModel::onMessageLongClicked, + onPhotoClicked = onNavigateToPhotoViewer, onPinnedMessageClicked = viewModel::onPinnedMessageClicked, onUnpinMessageButtonClicked = viewModel::onUnpinMessageClicked, onDeleteSelectedButtonClicked = viewModel::onDeleteSelectedMessagesClicked, diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt index 2dce8e09..a017e3a1 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -45,6 +44,7 @@ import dev.meloda.fast.messageshistory.model.UiItem import dev.meloda.fast.messageshistory.util.indexOfMessageByCmId import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.api.domain.VkMessage +import dev.meloda.fast.ui.components.Loader import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.util.ImmutableList @@ -68,6 +68,8 @@ fun MessagesHistoryScreen( baseError: BaseError? = null, canPaginate: Boolean = false, showEmojiButton: Boolean = false, + showAttachmentButton: Boolean = false, + enableHaptic: Boolean = false, onBack: () -> Unit = {}, onClose: () -> Unit = {}, onScrolledToIndex: () -> Unit = {}, @@ -81,6 +83,7 @@ fun MessagesHistoryScreen( onEmojiButtonLongClicked: () -> Unit = {}, onMessageClicked: (Long) -> Unit = {}, onMessageLongClicked: (Long) -> Unit = {}, + onPhotoClicked: (images: List, index: Int) -> Unit = { _, _ -> }, onPinnedMessageClicked: (Long) -> Unit = {}, onUnpinMessageButtonClicked: () -> Unit = {}, onDeleteSelectedButtonClicked: () -> Unit = {}, @@ -231,7 +234,8 @@ fun MessagesHistoryScreen( } currentOnMessageClicked.invoke(id) }, - onMessageLongClicked = onMessageLongClicked + onMessageLongClicked = onMessageLongClicked, + onPhotoClicked = onPhotoClicked ) MessagesHistoryInputBar( @@ -244,7 +248,9 @@ fun MessagesHistoryScreen( onLinkRequested = onLinkRequested, onRegularRequested = onRegularRequested, hazeState = hazeState, + enableHaptic = enableHaptic, showEmojiButton = showEmojiButton, + showAttachmentButton = showAttachmentButton, actionMode = screenState.actionMode, onSetMessageBarHeight = { messageBarHeight = it }, onEmojiButtonLongClicked = onEmojiButtonLongClicked, @@ -254,7 +260,7 @@ fun MessagesHistoryScreen( when { screenState.isLoading && messages.values.isEmpty() -> { - CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + Loader(modifier = Modifier.align(Alignment.Center)) } baseError != null -> { diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt index d2275d3a..74be64a1 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt @@ -23,7 +23,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -56,25 +56,36 @@ fun MessagesList( messageBarHeight: Dp, onRequestScrollToCmId: (cmId: Long) -> Unit = {}, onMessageClicked: (Long) -> Unit = {}, - onMessageLongClicked: (Long) -> Unit = {} + onMessageLongClicked: (Long) -> Unit = {}, + onPhotoClicked: (images: List, index: Int) -> Unit = { _, _ -> } ) { val context = LocalContext.current val theme = LocalThemeConfig.current val view = LocalView.current - val onAttachmentClick = remember { + val onAttachmentClick by rememberUpdatedState( { message: UiItem.Message, attachment: VkAttachment -> if (isSelectedAtLeastOne) { onMessageClicked(message.id) } else { when (attachment) { is VkPhotoDomain -> { - val maxSize = attachment.getMaxSize() - maxSize?.let { - context.startActivity( - Intent(Intent.ACTION_VIEW, maxSize.url.toUri()) - ) - } + val photos = message.attachments + .orEmpty() + .filterIsInstance() + .mapNotNull { photo -> photo.getMaxSize()?.url } + + onPhotoClicked( + photos, + photos.indexOfFirst { it == attachment.getMaxSize()?.url } + ) + +// val maxSize = attachment.getMaxSize() +// maxSize?.let { +// context.startActivity( +// Intent(Intent.ACTION_VIEW, maxSize.url.toUri()) +// ) +// } } is VkFileDomain -> { @@ -91,9 +102,9 @@ fun MessagesList( } } } - } + ) - val onAttachmentLongClick = remember { + val onAttachmentLongClick by rememberUpdatedState( { message: UiItem.Message, attachment: VkAttachment -> if (isSelectedAtLeastOne) { onMessageLongClicked(message.id) @@ -107,7 +118,7 @@ fun MessagesList( } } } - } + ) LazyColumn( modifier = modifier @@ -200,6 +211,7 @@ fun MessagesList( ), message = item, onClick = { attachment -> + onAttachmentClick(item, attachment) }, onLongClick = { attachment -> diff --git a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/PhotoViewViewModel.kt b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/PhotoViewViewModel.kt index 193a0d32..a469d1ac 100644 --- a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/PhotoViewViewModel.kt +++ b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/PhotoViewViewModel.kt @@ -1,21 +1,43 @@ package dev.meloda.fast.photoviewer +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.graphics.Bitmap +import android.widget.Toast +import androidx.core.content.FileProvider +import androidx.core.graphics.drawable.toBitmapOrNull import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import coil.imageLoader +import coil.request.ImageRequest import dev.meloda.fast.common.extensions.setValue import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.photoviewer.model.PhotoViewScreenState import dev.meloda.fast.photoviewer.navigation.PhotoView +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream import java.net.URLDecoder +import java.util.UUID interface PhotoViewViewModel { val screenState: StateFlow + + fun onPageChanged(newPage: Int) + + fun onCopyLinkClicked() + fun onCopyClicked() } class PhotoViewViewModelImpl( - savedStateHandle: SavedStateHandle + savedStateHandle: SavedStateHandle, + private val applicationContext: Context ) : PhotoViewViewModel, ViewModel() { override val screenState = MutableStateFlow(PhotoViewScreenState.EMPTY) @@ -25,10 +47,73 @@ class PhotoViewViewModelImpl( screenState.setValue { old -> old.copy( - images = arguments.images + images = arguments.imageUrls .map { URLDecoder.decode(it, "utf-8") } - .map(UiImage::Url) + .map(UiImage::Url), + selectedPage = arguments.selectedIndex?.takeIf { it != -1 } ?: 0 ) } } + + override fun onPageChanged(newPage: Int) { + screenState.setValue { old -> old.copy(selectedPage = newPage) } + } + + override fun onCopyLinkClicked() { + val url = screenState.value.images + .getOrNull(screenState.value.selectedPage) + ?.extractUrl() ?: return + + val clipboardManager = + applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + + clipboardManager.setPrimaryClip(ClipData.newPlainText("URL", url)) + + Toast.makeText( + applicationContext, + "URL copied to clipboard", + Toast.LENGTH_SHORT + ).show() + } + + override fun onCopyClicked() { + val clipboardManager = + applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + + val url = screenState.value.images + .getOrNull(screenState.value.selectedPage) + ?.extractUrl() ?: return + + viewModelScope.launch(Dispatchers.IO) { + val drawable = applicationContext.imageLoader.execute( + ImageRequest.Builder(applicationContext) + .data(url) + .build() + ).drawable ?: return@launch + + val imagesDir = File(applicationContext.cacheDir, "images") + if (!imagesDir.exists()) imagesDir.mkdirs() + val imageFile = File(imagesDir, "shared_image_id${UUID.randomUUID()}.png") + FileOutputStream(imageFile).use { + drawable.toBitmapOrNull()?.compress(Bitmap.CompressFormat.PNG, 100, it) + } + + val uri = FileProvider.getUriForFile( + applicationContext, + "${applicationContext.packageName}.provider", + imageFile + ) + + val clip = ClipData.newUri(applicationContext.contentResolver, "Image", uri) + clipboardManager.setPrimaryClip(clip) + + withContext(Dispatchers.Main) { + Toast.makeText( + applicationContext, + "Image copied to clipboard", + Toast.LENGTH_SHORT + ).show() + } + } + } } diff --git a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/model/PhotoViewArguments.kt b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/model/PhotoViewArguments.kt index dfecfe8c..f406224a 100644 --- a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/model/PhotoViewArguments.kt +++ b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/model/PhotoViewArguments.kt @@ -7,5 +7,6 @@ import kotlinx.serialization.Serializable @Parcelize @Serializable data class PhotoViewArguments( - val images: List + val imageUrls: List, + val selectedIndex: Int? ) : Parcelable diff --git a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/model/PhotoViewScreenState.kt b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/model/PhotoViewScreenState.kt index 112ded2d..9511987b 100644 --- a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/model/PhotoViewScreenState.kt +++ b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/model/PhotoViewScreenState.kt @@ -5,12 +5,14 @@ import dev.meloda.fast.common.model.UiImage @Immutable data class PhotoViewScreenState( - val images: List + val images: List, + val selectedPage: Int ) { companion object { val EMPTY: PhotoViewScreenState = PhotoViewScreenState( - images = emptyList() + images = emptyList(), + selectedPage = 0 ) } } diff --git a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/navigation/PhotoViewNavigation.kt b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/navigation/PhotoViewNavigation.kt index fb769f8a..b400ae5c 100644 --- a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/navigation/PhotoViewNavigation.kt +++ b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/navigation/PhotoViewNavigation.kt @@ -30,11 +30,15 @@ fun NavGraphBuilder.photoViewScreen( } } -fun NavController.navigateToPhotoView(images: List) { +fun NavController.navigateToPhotoView( + images: List, + selectedIndex: Int? = null +) { this.navigate( PhotoView( arguments = PhotoViewArguments( - images.map { URLEncoder.encode(it, "utf-8") } + imageUrls = images.map { URLEncoder.encode(it, "utf-8") }, + selectedIndex = selectedIndex ) ) ) diff --git a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/presentation/PhotoViewScreen.kt b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/presentation/PhotoViewScreen.kt index a756183e..6cb19d30 100644 --- a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/presentation/PhotoViewScreen.kt +++ b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/presentation/PhotoViewScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager @@ -14,27 +15,36 @@ import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.rounded.MoreVert +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.Scaffold +import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.painter.ColorPainter import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.compose.AsyncImage import com.conena.nanokt.android.content.pxToDp @@ -56,16 +66,30 @@ fun PhotoViewRoute( PhotoViewScreen( screenState = screenState, - onBack = onBack + onBack = onBack, + onPageChanged = viewModel::onPageChanged, + onCopyLinkClicked = viewModel::onCopyLinkClicked, + onCopyClicked = viewModel::onCopyClicked ) } @Composable fun PhotoViewScreen( screenState: PhotoViewScreenState = PhotoViewScreenState.EMPTY, - onBack: () -> Unit = {} + onBack: () -> Unit = {}, + onPageChanged: (index: Int) -> Unit = {}, + onCopyLinkClicked: () -> Unit = {}, + onCopyClicked: () -> Unit = {} ) { - val pagerState = rememberPagerState(pageCount = { screenState.images.size }) + val pagerState = rememberPagerState( + pageCount = { screenState.images.size }, + initialPage = screenState.selectedPage + ) + + LaunchedEffect(pagerState) { + snapshotFlow { pagerState.currentPage } + .collect(onPageChanged) + } var offsetY by remember { mutableFloatStateOf(0f) } @@ -81,7 +105,13 @@ fun PhotoViewScreen( Scaffold( modifier = Modifier.graphicsLayer(alpha = calculatedAlpha), - topBar = { TopBar(onBack = onBack) }, + topBar = { + TopBar( + onBack = onBack, + onCopyClicked = onCopyClicked, + onCopyLinkClicked = onCopyLinkClicked, + ) + }, containerColor = MaterialTheme.colorScheme.background.copy( alpha = calculatedAlpha ) @@ -103,14 +133,18 @@ fun PhotoViewScreen( @Composable fun TopBar( modifier: Modifier = Modifier, - onBack: () -> Unit + onBack: () -> Unit, + onCopyClicked: () -> Unit, + onCopyLinkClicked: () -> Unit ) { - val context = LocalContext.current - var dropdownMenuShown by remember { mutableStateOf(false) } + val hideDropDownMenu by rememberUpdatedState( + { dropdownMenuShown = false } + ) + TopAppBar( modifier = modifier, title = {}, @@ -123,29 +157,40 @@ fun TopBar( } }, actions = { -// IconButton.kt( -// onClick = { dropdownMenuShown = true } -// ) { -// Icon( -// imageVector = Icons.Rounded.MoreVert, -// contentDescription = "Options" -// ) -// } + IconButton( + onClick = { dropdownMenuShown = true } + ) { + Icon( + imageVector = Icons.Rounded.MoreVert, + contentDescription = "Options" + ) + } -// DropdownMenu( -// modifier = Modifier.defaultMinSize(minWidth = 140.dp), -// expanded = dropdownMenuShown, -// onDismissRequest = { dropdownMenuShown = false }, -// offset = DpOffset(x = (10).dp, y = (-60).dp) -// ) { -// DropdownMenuItem( -// onClick = { -// Toast.makeText(context, "Save clicked", Toast.LENGTH_SHORT).show() -// dropdownMenuShown = false -// }, -// text = { Text(text = "Save") }, -// ) -// } + DropdownMenu( + modifier = Modifier.defaultMinSize(minWidth = 140.dp), + expanded = dropdownMenuShown, + onDismissRequest = { dropdownMenuShown = false }, + offset = DpOffset(x = (10).dp, y = (-60).dp) + ) { + DropdownMenuItem( + onClick = { + hideDropDownMenu() + onCopyLinkClicked() + }, + text = { + Text(text = stringResource(UiR.string.action_copy_link)) + } + ) + DropdownMenuItem( + onClick = { + hideDropDownMenu() + onCopyClicked() + }, + text = { + Text(text = stringResource(UiR.string.action_copy_image)) + }, + ) + } }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent) ) @@ -230,7 +275,8 @@ private fun PhotoViewScreenPreview() { screenState = PhotoViewScreenState( images = List(200) { UiImage.Resource(UiR.drawable.test_captcha) - } + }, + selectedPage = 0 ) ) } diff --git a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt index f8932c03..5540ae09 100644 --- a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt @@ -201,24 +201,12 @@ class SettingsViewModelImpl( userSettings.onAppLanguageChanged(newLanguage) } - - SettingsKeys.DEFAULT_VALUE_FEATURES_FAST_TEXT -> { - val newText = newValue as? String ?: SettingsKeys.DEFAULT_VALUE_FEATURES_FAST_TEXT - userSettings.onFastTextChanged(newText) - } - - SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS -> { val isUsing = newValue as? Boolean ?: SettingsKeys.DEFAULT_VALUE_KEY_ACTIVITY_SEND_ONLINE_STATUS userSettings.onSendOnlineStatusChanged(isUsing) } - SettingsKeys.KEY_DEBUG_SHOW_CRASH_ALERT -> { - val show = newValue as? Boolean ?: SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON - userSettings.onShowAlertAfterCrashChanged(show) - } - SettingsKeys.KEY_LONG_POLL_IN_BACKGROUND -> { val inBackground = newValue as? Boolean ?: SettingsKeys.DEFAULT_LONG_POLL_IN_BACKGROUND @@ -240,17 +228,6 @@ class SettingsViewModelImpl( userSettings.onUseBlurChanged(isUsing) } - SettingsKeys.KEY_SHOW_EMOJI_BUTTON -> { - val show = newValue as? Boolean ?: SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON - userSettings.onShowEmojiButtonChanged(show) - } - - SettingsKeys.KEY_SHOW_TIME_IN_ACTION_MESSAGES -> { - val show = newValue as? Boolean - ?: SettingsKeys.DEFAULT_SHOW_TIME_IN_ACTION_MESSAGES - userSettings.onShowTimeInActionMessagesChanged(show) - } - SettingsKeys.KEY_USE_SYSTEM_FONT -> { val use = newValue as? Boolean ?: SettingsKeys.DEFAULT_USE_SYSTEM_FONT userSettings.onUseSystemFontChanged(use) @@ -302,6 +279,12 @@ class SettingsViewModelImpl( text = UiText.Resource(UiR.string.settings_general_show_emoji_button_summary), defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON ) + val generalShowAttachmentButton = SettingsItem.Switch( + key = SettingsKeys.KEY_SHOW_ATTACHMENT_BUTTON, + title = UiText.Resource(UiR.string.settings_general_show_attachment_button_title), + text = UiText.Resource(UiR.string.settings_general_show_attachment_button_summary), + defaultValue = SettingsKeys.DEFAULT_VALUE_SHOW_ATTACHMENT_BUTTON + ) val generalEnableHaptic = SettingsItem.Switch( key = SettingsKeys.KEY_ENABLE_HAPTIC, defaultValue = SettingsKeys.DEFAULT_ENABLE_HAPTIC, @@ -476,6 +459,7 @@ class SettingsViewModelImpl( generalTitle, generalUseContactNames, generalShowEmojiButton, + generalShowAttachmentButton, generalEnableHaptic ) val appearanceList = listOf(