diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 7d687978148..b602898eeb7 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -64,11 +64,16 @@ import androidx.cardview.widget.CardView import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.ComposeView import androidx.coordinatorlayout.widget.CoordinatorLayout @@ -117,6 +122,8 @@ import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.api.NcApiCoroutines import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.chat.data.model.ChatMessage +import com.nextcloud.talk.chat.data.model.FileParameters +import com.nextcloud.talk.chat.ui.ShowReactionsModalBottomSheet import com.nextcloud.talk.chat.ui.model.MessageTypeContent import com.nextcloud.talk.chat.viewmodels.ChatViewModel import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel @@ -162,7 +169,6 @@ import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment import com.nextcloud.talk.ui.dialog.GetPinnedOptionsDialog import com.nextcloud.talk.ui.dialog.MessageActionsDialog import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment -import com.nextcloud.talk.chat.ui.ShowReactionsModalBottomSheet import com.nextcloud.talk.ui.dialog.TempMessageActionsDialog import com.nextcloud.talk.ui.theme.LocalMessageUtils import com.nextcloud.talk.ui.theme.LocalOpenGraphFetcher @@ -603,6 +609,7 @@ class ChatActivity : binding.messagesListViewCompose.visibility = View.VISIBLE val listState = rememberLazyListState() + SideEffect { chatListState = listState } CompositionLocalProvider( @@ -613,13 +620,26 @@ class ChatActivity : val isOneToOneConversation = uiState.isOneToOneConversation Log.d(TAG, "isOneToOneConversation=" + isOneToOneConversation) + // list of the file ids of messages being downloaded + val downloadingFileState = remember { mutableStateOf(listOf()) } + + // openWhenDownloaded is a derived boolean state of the visible chat message list on the condition + // that if any of the messages that are present contain a fileId that is within downloadingFileState + val openWhenDownloadState = remember { mutableStateOf(false) } + + val visibleIds = listState.visibleItemsWithThreshold() + LaunchedEffect(visibleIds, downloadingFileState.value) { + openWhenDownloadState.value = (downloadingFileState.value.intersect(visibleIds).isNotEmpty()) + } + ChatView( state = ChatViewState( chatItems = uiState.items, isOneToOneConversation = isOneToOneConversation, conversationThreadId = conversationThreadId, hasChatPermission = this::participantPermissions.isInitialized && - participantPermissions.hasChatPermission() + participantPermissions.hasChatPermission(), + downloadingFileState = downloadingFileState.value ), callbacks = ChatViewCallbacks( onLoadMore = { loadMoreMessagesCompose() }, @@ -627,7 +647,7 @@ class ChatActivity : updateRemoteLastReadMessageIfNeeded = { updateRemoteLastReadMessageIfNeeded() }, onLongClick = { openMessageActionsDialog(it) }, onSwipeReply = { handleSwipeToReply(it) }, - onFileClick = { downloadAndOpenFile(it) }, + onFileClick = { downloadAndOpenFile(it, openWhenDownloadState, downloadingFileState) }, onPollClick = { pollId, pollName -> openPollDialog(pollId, pollName) }, onVoicePlayPauseClick = { onVoicePlayPauseClickCompose(it) }, onVoiceSeek = { _, progress -> chatViewModel.seekToMediaPlayer(progress) }, @@ -662,6 +682,39 @@ class ChatActivity : } } + @Composable + private fun LazyListState.visibleItemsWithThreshold(): List = + remember(this) { + derivedStateOf { + val visibleItemsInfo = layoutInfo.visibleItemsInfo + if (layoutInfo.totalItemsCount == 0) { + emptyList() + } else { + visibleItemsInfo.toMutableList().map { it.key as String } + } + } + }.value.mapNotNull { key -> + val messageItem = chatViewModel.uiState.value.items.firstOrNull { it.stableKey() == key } + val message = messageItem?.messageOrNull() + var result: String? = null + message?.let { + if (message.messageParameters.isNotEmpty()) { + runCatching { + message.messageParameters as HashMap>? + val fileParameters = FileParameters(message.messageParameters) + result = fileParameters.id + }.onFailure { e -> + when (e) { + is ClassCastException -> {} // weird + else -> Log.e(TAG, "Error in LazyListState.visibleItemsWithThreshold $e") + } + } + } + } + + result + } + private fun onLoadQuotedMessage(messageId: Int) { // Loading and displaying surrounding messages for quotes is pending; replace flow from latestChatBlock with // other flow @@ -730,10 +783,18 @@ class ChatActivity : chatViewModel.setVoiceMessageSpeed(messageId, nextSpeed) } - fun downloadAndOpenFile(messageId: Int) { + fun downloadAndOpenFile( + messageId: Int, + openWhenDownloadState: MutableState, + downloadState: MutableState> + ) { lifecycleScope.launch { val chatMessage = chatViewModel.getMessageById(messageId.toLong()).first() - FileViewerUtils(this@ChatActivity, conversationUser).openFile(chatMessage) + FileViewerUtils(this@ChatActivity, conversationUser).openFile( + chatMessage, + openWhenDownloadState, + downloadState + ) } } @@ -3558,7 +3619,6 @@ class ChatActivity : if (noteToSelfConversation != null) { var shareUri: Uri? = null - val data: HashMap? var metaData = "" var objectId = "" if (message.hasFileAttachment) { diff --git a/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt b/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt index 266c44c8e81..8113ee3d840 100644 --- a/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt @@ -11,6 +11,7 @@ import android.content.Context import android.view.View import android.widget.ImageView import android.widget.ProgressBar +import androidx.compose.runtime.mutableStateOf import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding import com.nextcloud.talk.data.user.model.User @@ -58,6 +59,7 @@ abstract class SharedItemsViewHolder( This should be done after a refactoring of FileViewerUtils. */ val fileViewerUtils = FileViewerUtils(image.context, user) + val trueState = mutableStateOf(true) clickTarget.setOnClickListener { fileViewerUtils.openFile( @@ -74,7 +76,7 @@ abstract class SharedItemsViewHolder( // null, // image // ), - true + trueState ) } @@ -82,7 +84,7 @@ abstract class SharedItemsViewHolder( item.name, item.id, item.mimeType, - true, + trueState, FileViewerUtils.ProgressUi(progressBar, null, image) ) } diff --git a/app/src/main/java/com/nextcloud/talk/ui/chat/ChatMessageView.kt b/app/src/main/java/com/nextcloud/talk/ui/chat/ChatMessageView.kt index e766b6ee174..b3a915827d7 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/chat/ChatMessageView.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/chat/ChatMessageView.kt @@ -51,10 +51,11 @@ private const val QUOTE_HIGHLIGHT_FADE_OUT_MILLIS = 1500 data class ChatMessageContext( val isOneToOneConversation: Boolean = false, val conversationThreadId: Long? = null, - val hasChatPermission: Boolean = true + val hasChatPermission: Boolean = true, + val downloadingFileState: List = listOf() ) -class ChatMessageCallbacks( +data class ChatMessageCallbacks( val onLongClick: ((Int) -> Unit?)? = null, val onSwipeReply: ((Int) -> Unit)? = null, val onFileClick: (Int) -> Unit = {}, @@ -68,6 +69,7 @@ class ChatMessageCallbacks( val onQuotedMessageClick: (Int) -> Unit = {} ) +@Suppress("TooLongMethod") @Composable fun ChatMessageView( message: ChatMessageUi, @@ -130,6 +132,7 @@ fun ChatMessageView( message = message, isOneToOneConversation = context.isOneToOneConversation, conversationThreadId = context.conversationThreadId, + chatViewDownloadingFileState = context.downloadingFileState, onImageClick = callbacks.onFileClick ) } diff --git a/app/src/main/java/com/nextcloud/talk/ui/chat/ChatView.kt b/app/src/main/java/com/nextcloud/talk/ui/chat/ChatView.kt index 59c77419002..1e1c21c79c9 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/chat/ChatView.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/chat/ChatView.kt @@ -15,8 +15,8 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues @@ -25,8 +25,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape @@ -57,6 +57,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.nextcloud.talk.R import com.nextcloud.talk.chat.ui.model.ChatMessageUi import com.nextcloud.talk.chat.ui.model.MessageStatusIcon import com.nextcloud.talk.chat.ui.model.MessageTypeContent @@ -72,7 +73,6 @@ import java.time.Instant import java.time.LocalDate import java.time.ZoneId import java.time.format.DateTimeFormatter -import com.nextcloud.talk.R private const val LONG_1000 = 1000L private const val LOAD_MORE_BUFFER_ITEMS = 5 @@ -88,10 +88,11 @@ data class ChatViewState( val conversationThreadId: Long? = null, val hasChatPermission: Boolean = true, val initialUnreadCount: Int = 0, - val initialShowUnreadPopup: Boolean = false + val initialShowUnreadPopup: Boolean = false, + val downloadingFileState: List = listOf() ) -class ChatViewCallbacks( +data class ChatViewCallbacks( val onLoadMore: (() -> Unit?)? = null, val advanceLocalLastReadMessageIfNeeded: ((Int) -> Unit?)? = null, val updateRemoteLastReadMessageIfNeeded: (() -> Unit?)? = null, @@ -311,7 +312,8 @@ fun ChatView( context = ChatMessageContext( isOneToOneConversation = state.isOneToOneConversation, conversationThreadId = state.conversationThreadId, - hasChatPermission = state.hasChatPermission + hasChatPermission = state.hasChatPermission, + downloadingFileState = state.downloadingFileState ), callbacks = ChatMessageCallbacks( onLongClick = callbacks.onLongClick, diff --git a/app/src/main/java/com/nextcloud/talk/ui/chat/MediaMessage.kt b/app/src/main/java/com/nextcloud/talk/ui/chat/MediaMessage.kt index d0964326cab..23e5a0ac2e5 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/chat/MediaMessage.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/chat/MediaMessage.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -28,6 +29,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.nextcloud.talk.R +import com.nextcloud.talk.chat.data.model.FileParameters import com.nextcloud.talk.chat.ui.model.ChatMessageUi import com.nextcloud.talk.chat.ui.model.MessageTypeContent import com.nextcloud.talk.contacts.load @@ -37,15 +39,19 @@ private const val FILE_PLACEHOLDER_MESSAGE = "{file}" private val mediaRadiusBig = 8.dp private val mediaRadiusSmall = 2.dp -@Suppress("Detekt.LongMethod") +@Suppress("Detekt.LongMethod", "LongParameterList") @Composable fun MediaMessage( typeContent: MessageTypeContent.Media, message: ChatMessageUi, isOneToOneConversation: Boolean = false, conversationThreadId: Long? = null, + chatViewDownloadingFileState: List, onImageClick: (Int) -> Unit ) { + val fileParameters = + remember { FileParameters(message.messageParameters as HashMap>?) } + val captionText = message.message.takeUnless { it == FILE_PLACEHOLDER_MESSAGE } val hasCaption = captionText != null val mediaInset = 4.dp @@ -111,6 +117,15 @@ fun MediaMessage( tint = Color.White ) } + + if (chatViewDownloadingFileState.contains(fileParameters.id)) { + CircularProgressIndicator( + modifier = Modifier + .size(48.dp) + .align(Alignment.Center), + strokeWidth = 2.dp + ) + } } } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt index 45602091ba7..7ab6ac240f5 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt @@ -15,10 +15,11 @@ import android.util.Log import android.view.View import android.widget.ImageView import android.widget.ProgressBar -import androidx.lifecycle.Observer +import androidx.compose.runtime.MutableState import androidx.core.content.FileProvider import androidx.core.net.toUri import androidx.emoji2.widget.EmojiTextView +import androidx.lifecycle.Observer import androidx.work.Data import androidx.work.OneTimeWorkRequest import androidx.work.WorkInfo @@ -61,7 +62,11 @@ import java.util.concurrent.ExecutionException */ class FileViewerUtils(private val context: Context, private val user: User) { - fun openFile(message: ChatMessage) { + fun openFile( + message: ChatMessage, + openWhenDownloadState: MutableState, + downloadState: MutableState> + ) { val fileName = message.fileParameters.name val mimetype = message.fileParameters.mimetype val link = message.fileParameters.link @@ -71,19 +76,26 @@ class FileViewerUtils(private val context: Context, private val user: User) { val fileSize = message.fileParameters.size + Log.d("Julius", "OpenWhenDownloaded: ${openWhenDownloadState.value}") openFile( FileInfo(fileId, fileName, fileSize, path, link, mimetype), - message.openWhenDownloaded + openWhenDownloadState, + downloadState ) } - fun openFile(fileInfo: FileInfo, openWhenDownloaded: Boolean) { + fun openFile( + fileInfo: FileInfo, + openWhenDownloadState: MutableState, + downloadState: MutableState>? = null + ) { if (isSupportedForInternalViewer(fileInfo.mimetype) || canBeHandledByExternalApp(fileInfo.mimetype, fileInfo.fileName) ) { openOrDownloadFile( fileInfo, - openWhenDownloaded + openWhenDownloadState, + downloadState ) } else if (!fileInfo.link.isNullOrEmpty()) { openFileInFilesApp(fileInfo.link, fileInfo.fileId) @@ -106,14 +118,19 @@ class FileViewerUtils(private val context: Context, private val user: User) { return intent.resolveActivity(context.packageManager) != null } - private fun openOrDownloadFile(fileInfo: FileInfo, openWhenDownloaded: Boolean) { + private fun openOrDownloadFile( + fileInfo: FileInfo, + openWhenDownloadState: MutableState, + downloadState: MutableState>? + ) { val file = File(context.cacheDir, fileInfo.fileName) if (file.exists()) { openFileByMimetype(fileInfo.fileName, fileInfo.mimetype, fileInfo.link, fileInfo.fileId) } else { downloadFileToCache( fileInfo, - openWhenDownloaded + openWhenDownloadState, + downloadState ) } } @@ -127,18 +144,17 @@ class FileViewerUtils(private val context: Context, private val user: User) { VIDEO_MP4, VIDEO_QUICKTIME, VIDEO_OGG, - VIDEO_WEBM - -> openMediaView(filename, mimetype) + VIDEO_WEBM -> openMediaView(filename, mimetype) + IMAGE_PNG, IMAGE_JPEG, IMAGE_GIF, - IMAGE_HEIC - -> openImageView(filename, mimetype) + IMAGE_HEIC -> openImageView(filename, mimetype) + TEXT_MARKDOWN, - TEXT_PLAIN - -> openTextView(filename, mimetype, link, fileId) - else - -> openFileByExternalApp(filename, mimetype) + TEXT_PLAIN -> openTextView(filename, mimetype, link, fileId) + + else -> openFileByExternalApp(filename, mimetype) } } else { Log.e(TAG, "can't open file with unknown mimetype") @@ -237,11 +253,18 @@ class FileViewerUtils(private val context: Context, private val user: User) { VIDEO_WEBM, TEXT_MARKDOWN, TEXT_PLAIN -> true + else -> false } @SuppressLint("LongLogTag") - private fun downloadFileToCache(fileInfo: FileInfo, openWhenDownloaded: Boolean) { + private fun downloadFileToCache( + fileInfo: FileInfo, + openWhenDownloadState: MutableState, + downloadState: MutableState>? + ) { + downloadState?.value = downloadState.value + fileInfo.fileId + // check if download worker is already running val workers = WorkManager.getInstance(context).getWorkInfosByTag(fileInfo.fileId) try { @@ -289,24 +312,26 @@ class FileViewerUtils(private val context: Context, private val user: User) { fileInfo.fileName, fileInfo.mimetype, workInfo, - openWhenDownloaded, + openWhenDownloadState, fileInfo.link, fileInfo.fileId ) if (workInfo.state.isFinished) { liveData.removeObserver(this) + downloadState?.value = downloadState.value - fileInfo.fileId } } } liveData.observeForever(observer) } + @Suppress("LongParameterList") private fun updateViewsByProgress( fileName: String, mimetype: String?, workInfo: WorkInfo, // progressUi: ProgressUi, - openWhenDownloaded: Boolean, + openWhenDownloadState: MutableState, link: String? = null, fileId: String = "" ) { @@ -321,8 +346,9 @@ class FileViewerUtils(private val context: Context, private val user: User) { // ) } } + WorkInfo.State.SUCCEEDED -> { - if (openWhenDownloaded) { + if (openWhenDownloadState.value) { openFileByMimetype(fileName, mimetype, link, fileId) } else { Log.d( @@ -335,10 +361,12 @@ class FileViewerUtils(private val context: Context, private val user: User) { // progressUi.messageText?.text = fileName // progressUi.progressBar?.visibility = View.GONE } + WorkInfo.State.FAILED -> { // progressUi.messageText?.text = fileName // progressUi.progressBar?.visibility = View.GONE } + else -> { } } @@ -348,7 +376,7 @@ class FileViewerUtils(private val context: Context, private val user: User) { fileName: String, fileId: String, mimeType: String?, - openWhenDownloaded: Boolean, + openWhenDownloadState: MutableState, progressUi: ProgressUi ) { val workers = WorkManager.getInstance(context).getWorkInfosByTag(fileId) @@ -367,7 +395,7 @@ class FileViewerUtils(private val context: Context, private val user: User) { fileName, mimeType, info!!, - openWhenDownloaded + openWhenDownloadState ) } }