Skip to content

Commit af319a8

Browse files
authored
Merge pull request #6096 from nextcloud/issue-6068-reimpl-download-progress
Reimplementing download progress and open when downloaded functionality
2 parents 50452ab + 4e58d07 commit af319a8

6 files changed

Lines changed: 148 additions & 37 deletions

File tree

app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,17 @@ import androidx.compose.foundation.gestures.scrollBy
6767
import androidx.compose.foundation.lazy.LazyListState
6868
import androidx.compose.foundation.lazy.rememberLazyListState
6969
import androidx.compose.material3.MaterialTheme
70+
import androidx.compose.runtime.Composable
7071
import androidx.compose.runtime.CompositionLocalProvider
72+
import androidx.compose.runtime.LaunchedEffect
73+
import androidx.compose.runtime.MutableState
7174
import androidx.compose.runtime.SideEffect
75+
import androidx.compose.runtime.collectAsState
76+
import androidx.compose.runtime.derivedStateOf
7277
import androidx.compose.runtime.getValue
7378
import androidx.compose.runtime.mutableStateOf
7479
import androidx.compose.runtime.produceState
80+
import androidx.compose.runtime.remember
7581
import androidx.compose.runtime.rememberCoroutineScope
7682
import androidx.compose.runtime.setValue
7783
import androidx.compose.ui.platform.ComposeView
@@ -121,6 +127,7 @@ import com.nextcloud.talk.api.NcApi
121127
import com.nextcloud.talk.api.NcApiCoroutines
122128
import com.nextcloud.talk.application.NextcloudTalkApplication
123129
import com.nextcloud.talk.chat.data.model.ChatMessage
130+
import com.nextcloud.talk.chat.data.model.FileParameters
124131
import com.nextcloud.talk.chat.ui.ShowReactionsModalBottomSheet
125132
import com.nextcloud.talk.chat.ui.model.MessageTypeContent
126133
import com.nextcloud.talk.chat.viewmodels.ChatViewModel
@@ -220,9 +227,9 @@ import kotlinx.coroutines.FlowPreview
220227
import kotlinx.coroutines.Job
221228
import kotlinx.coroutines.delay
222229
import kotlinx.coroutines.flow.collect
223-
import kotlinx.coroutines.flow.first
224230
import kotlinx.coroutines.flow.collectLatest
225231
import kotlinx.coroutines.flow.distinctUntilChanged
232+
import kotlinx.coroutines.flow.first
226233
import kotlinx.coroutines.flow.map
227234
import kotlinx.coroutines.flow.onEach
228235
import kotlinx.coroutines.launch
@@ -725,6 +732,8 @@ class ChatActivity :
725732
chatListComposeScope = composeScope
726733
}
727734

735+
SideEffect { chatListState = listState }
736+
728737
CompositionLocalProvider(
729738
LocalViewThemeUtils provides viewThemeUtils,
730739
LocalMessageUtils provides messageUtils,
@@ -733,6 +742,18 @@ class ChatActivity :
733742
val isOneToOneConversation = uiState.isOneToOneConversation
734743
Log.d(TAG, "isOneToOneConversation=" + isOneToOneConversation)
735744

745+
// list of the file ids of messages being downloaded
746+
val downloadingFileState = remember { mutableStateOf(listOf<String>()) }
747+
748+
// openWhenDownloaded is a derived boolean state of the visible chat message list on the condition
749+
// that if any of the messages that are present contain a fileId that is within downloadingFileState
750+
val openWhenDownloadState = remember { mutableStateOf(false) }
751+
752+
val visibleIds = listState.visibleItemsWithThreshold()
753+
LaunchedEffect(visibleIds, downloadingFileState.value) {
754+
openWhenDownloadState.value = (downloadingFileState.value.intersect(visibleIds).isNotEmpty())
755+
}
756+
736757
ChatView(
737758
state = ChatViewState(
738759
chatItems = uiState.items,
@@ -742,7 +763,8 @@ class ChatActivity :
742763
highlightedMessageId = uiState.highlightedMessageId,
743764
highlightedSearchTerm = uiState.highlightedSearchTerm,
744765
hasChatPermission = this::participantPermissions.isInitialized &&
745-
participantPermissions.hasChatPermission()
766+
participantPermissions.hasChatPermission(),
767+
downloadingFileState = downloadingFileState.value
746768
),
747769
callbacks = ChatViewCallbacks(
748770
onLoadMore = { messageId, direction -> loadMoreMessages(messageId, direction) },
@@ -753,7 +775,7 @@ class ChatActivity :
753775
messageCallbacks = ChatMessageCallbacks(
754776
onLongClick = { openMessageActionsDialog(it) },
755777
onSwipeReply = { handleSwipeToReply(it) },
756-
onFileClick = { downloadAndOpenFile(it) },
778+
onFileClick = { downloadAndOpenFile(it, openWhenDownloadState, downloadingFileState) },
757779
onPollClick = { pollId, pollName -> openPollDialog(pollId, pollName) },
758780
onVoicePlayPauseClick = { onVoicePlayPauseClickCompose(it) },
759781
onVoiceSeek = { _, progress -> chatViewModel.seekToMediaPlayer(progress) },
@@ -791,6 +813,39 @@ class ChatActivity :
791813
}
792814
}
793815

816+
@Composable
817+
private fun LazyListState.visibleItemsWithThreshold(): List<String> =
818+
remember(this) {
819+
derivedStateOf {
820+
val visibleItemsInfo = layoutInfo.visibleItemsInfo
821+
if (layoutInfo.totalItemsCount == 0) {
822+
emptyList()
823+
} else {
824+
visibleItemsInfo.mapNotNull { it.key as? String }
825+
}
826+
}
827+
}.value.mapNotNull { key ->
828+
val messageItem = chatViewModel.uiState.collectAsState().value.items.firstOrNull { it.stableKey() == key }
829+
val message = messageItem?.messageOrNull()
830+
var result: String? = null
831+
message?.let {
832+
if (message.messageParameters.isNotEmpty()) {
833+
runCatching {
834+
message.messageParameters as HashMap<String?, HashMap<String?, String?>>?
835+
val fileParameters = FileParameters(message.messageParameters)
836+
result = fileParameters.id
837+
}.onFailure { e ->
838+
when (e) {
839+
is ClassCastException -> {} // weird
840+
else -> Log.e(TAG, "Error in LazyListState.visibleItemsWithThreshold $e")
841+
}
842+
}
843+
}
844+
}
845+
846+
result
847+
}
848+
794849
private fun onLoadQuotedMessage(messageId: Int) {
795850
chatViewModel.jumpToQuotedMessage(messageId.toLong())
796851
}
@@ -856,10 +911,18 @@ class ChatActivity :
856911
chatViewModel.setVoiceMessageSpeed(messageId, nextSpeed)
857912
}
858913

859-
fun downloadAndOpenFile(messageId: Int) {
914+
fun downloadAndOpenFile(
915+
messageId: Int,
916+
openWhenDownloadState: MutableState<Boolean>,
917+
downloadState: MutableState<List<String>>
918+
) {
860919
lifecycleScope.launch {
861920
val chatMessage = chatViewModel.getMessageById(messageId.toLong()).first()
862-
FileViewerUtils(this@ChatActivity, conversationUser).openFile(chatMessage)
921+
FileViewerUtils(this@ChatActivity, conversationUser).openFile(
922+
chatMessage,
923+
openWhenDownloadState,
924+
downloadState
925+
)
863926
}
864927
}
865928

app/src/main/java/com/nextcloud/talk/shareditems/adapters/SharedItemsViewHolder.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import android.content.Context
1111
import android.view.View
1212
import android.widget.ImageView
1313
import android.widget.ProgressBar
14+
import androidx.compose.runtime.mutableStateOf
1415
import androidx.recyclerview.widget.RecyclerView
1516
import androidx.viewbinding.ViewBinding
1617
import com.nextcloud.talk.data.user.model.User
@@ -58,6 +59,7 @@ abstract class SharedItemsViewHolder(
5859
This should be done after a refactoring of FileViewerUtils.
5960
*/
6061
val fileViewerUtils = FileViewerUtils(image.context, user)
62+
val trueState = mutableStateOf(true)
6163

6264
clickTarget.setOnClickListener {
6365
fileViewerUtils.openFile(
@@ -74,15 +76,15 @@ abstract class SharedItemsViewHolder(
7476
// null,
7577
// image
7678
// ),
77-
true
79+
trueState
7880
)
7981
}
8082

8183
fileViewerUtils.resumeToUpdateViewsByProgress(
8284
item.name,
8385
item.id,
8486
item.mimeType,
85-
true,
87+
trueState,
8688
FileViewerUtils.ProgressUi(progressBar, null, image)
8789
)
8890
}

app/src/main/java/com/nextcloud/talk/ui/chat/ChatMessageView.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,12 @@ private const val QUOTE_HIGHLIGHT_FADE_OUT_MILLIS = 1500
5454
data class ChatMessageContext(
5555
val isOneToOneConversation: Boolean = false,
5656
val conversationThreadId: Long? = null,
57-
val hasChatPermission: Boolean = true
57+
val hasChatPermission: Boolean = true,
58+
val downloadingFileState: List<String> = listOf()
5859
)
5960

6061
@Suppress("Detekt.LongParameterList")
61-
class ChatMessageCallbacks(
62+
data class ChatMessageCallbacks(
6263
val onLongClick: ((Int) -> Unit?)? = null,
6364
val onSwipeReply: ((Int) -> Unit)? = null,
6465
val onFileClick: (Int) -> Unit = {},
@@ -149,6 +150,7 @@ fun ChatMessageView(
149150
message = message,
150151
isOneToOneConversation = context.isOneToOneConversation,
151152
conversationThreadId = context.conversationThreadId,
153+
chatViewDownloadingFileState = context.downloadingFileState,
152154
onImageClick = callbacks.onFileClick
153155
)
154156
}

app/src/main/java/com/nextcloud/talk/ui/chat/ChatView.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import androidx.compose.animation.fadeOut
1515
import androidx.compose.animation.scaleIn
1616
import androidx.compose.animation.scaleOut
1717
import androidx.compose.foundation.ExperimentalFoundationApi
18-
import androidx.compose.foundation.gestures.animateScrollBy
1918
import androidx.compose.foundation.background
19+
import androidx.compose.foundation.gestures.animateScrollBy
2020
import androidx.compose.foundation.layout.Arrangement
2121
import androidx.compose.foundation.layout.Box
2222
import androidx.compose.foundation.layout.PaddingValues
@@ -25,8 +25,8 @@ import androidx.compose.foundation.layout.fillMaxSize
2525
import androidx.compose.foundation.layout.fillMaxWidth
2626
import androidx.compose.foundation.layout.padding
2727
import androidx.compose.foundation.layout.size
28-
import androidx.compose.foundation.lazy.LazyListState
2928
import androidx.compose.foundation.lazy.LazyColumn
29+
import androidx.compose.foundation.lazy.LazyListState
3030
import androidx.compose.foundation.lazy.items
3131
import androidx.compose.foundation.lazy.rememberLazyListState
3232
import androidx.compose.foundation.shape.CircleShape
@@ -59,6 +59,7 @@ import androidx.compose.ui.res.stringResource
5959
import androidx.compose.ui.tooling.preview.Preview
6060
import androidx.compose.ui.unit.dp
6161
import androidx.compose.ui.unit.sp
62+
import com.nextcloud.talk.R
6263
import com.nextcloud.talk.chat.ui.model.ChatMessageUi
6364
import com.nextcloud.talk.chat.ui.model.MessageStatusIcon
6465
import com.nextcloud.talk.chat.ui.model.MessageTypeContent
@@ -74,7 +75,6 @@ import java.time.Instant
7475
import java.time.LocalDate
7576
import java.time.ZoneId
7677
import java.time.format.DateTimeFormatter
77-
import com.nextcloud.talk.R
7878

7979
private const val LONG_1000 = 1000L
8080
private const val LOAD_MORE_BUFFER_ITEMS = 5
@@ -93,10 +93,11 @@ data class ChatViewState(
9393
val initialShowUnreadPopup: Boolean = false,
9494
val chatMode: ChatViewModel.ChatMode = ChatViewModel.ChatMode.DEFAULT_MODE,
9595
val highlightedMessageId: Int? = null,
96-
val highlightedSearchTerm: String? = null
96+
val highlightedSearchTerm: String? = null,
97+
val downloadingFileState: List<String> = listOf()
9798
)
9899

99-
class ChatViewCallbacks(
100+
data class ChatViewCallbacks(
100101
val onLoadMore: ((Int, ChatViewModel.LoadMoreDirection) -> Unit?)? = null,
101102
val advanceLocalLastReadMessageIfNeeded: ((Int) -> Unit?)? = null,
102103
val updateRemoteLastReadMessageIfNeeded: (() -> Unit?)? = null,
@@ -349,7 +350,8 @@ fun ChatView(
349350
context = ChatMessageContext(
350351
isOneToOneConversation = state.isOneToOneConversation,
351352
conversationThreadId = state.conversationThreadId,
352-
hasChatPermission = state.hasChatPermission
353+
hasChatPermission = state.hasChatPermission,
354+
downloadingFileState = state.downloadingFileState
353355
),
354356
callbacks = ChatMessageCallbacks(
355357
onLongClick = callbacks.messageCallbacks.onLongClick,

app/src/main/java/com/nextcloud/talk/ui/chat/MediaMessage.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
1414
import androidx.compose.foundation.layout.padding
1515
import androidx.compose.foundation.layout.size
1616
import androidx.compose.foundation.shape.RoundedCornerShape
17+
import androidx.compose.material3.CircularProgressIndicator
1718
import androidx.compose.material3.Icon
1819
import androidx.compose.runtime.Composable
1920
import androidx.compose.runtime.remember
@@ -28,6 +29,7 @@ import androidx.compose.ui.res.stringResource
2829
import androidx.compose.ui.unit.dp
2930
import coil.compose.AsyncImage
3031
import com.nextcloud.talk.R
32+
import com.nextcloud.talk.chat.data.model.FileParameters
3133
import com.nextcloud.talk.chat.ui.model.ChatMessageUi
3234
import com.nextcloud.talk.chat.ui.model.MessageTypeContent
3335
import com.nextcloud.talk.contacts.load
@@ -38,15 +40,19 @@ private const val FILE_PLACEHOLDER_MESSAGE = "{file}"
3840
private val mediaRadiusBig = 8.dp
3941
private val mediaRadiusSmall = 2.dp
4042

41-
@Suppress("Detekt.LongMethod")
43+
@Suppress("Detekt.LongMethod", "LongParameterList")
4244
@Composable
4345
fun MediaMessage(
4446
typeContent: MessageTypeContent.Media,
4547
message: ChatMessageUi,
4648
isOneToOneConversation: Boolean = false,
4749
conversationThreadId: Long? = null,
50+
chatViewDownloadingFileState: List<String>,
4851
onImageClick: (Int) -> Unit
4952
) {
53+
val fileParameters =
54+
remember { FileParameters(message.messageParameters as HashMap<String?, HashMap<String?, String?>>?) }
55+
5056
val captionText = message.message.takeUnless { it == FILE_PLACEHOLDER_MESSAGE }
5157
val hasCaption = captionText != null
5258
val mediaInset = 4.dp
@@ -118,6 +124,15 @@ fun MediaMessage(
118124
tint = Color.White
119125
)
120126
}
127+
128+
if (chatViewDownloadingFileState.contains(fileParameters.id)) {
129+
CircularProgressIndicator(
130+
modifier = Modifier
131+
.size(48.dp)
132+
.align(Alignment.Center),
133+
strokeWidth = 2.dp
134+
)
135+
}
121136
}
122137
}
123138
}

0 commit comments

Comments
 (0)