Skip to content

Commit

Permalink
feat: media gallery [WPB-4989] (#2490)
Browse files Browse the repository at this point in the history
  • Loading branch information
Garzas committed Dec 4, 2023
1 parent 7e67653 commit fc6e59e
Show file tree
Hide file tree
Showing 20 changed files with 871 additions and 98 deletions.
Expand Up @@ -19,11 +19,9 @@ package com.wire.android.di.accountScoped

import com.wire.android.di.CurrentAccount
import com.wire.android.di.KaliumCoreLogic
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.asset.GetAssetMessagesForConversationUseCase
import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase
import com.wire.kalium.logic.feature.asset.ScheduleNewAssetMessageUseCase
import com.wire.kalium.logic.feature.asset.UpdateAssetMessageDownloadStatusUseCase
Expand All @@ -47,6 +45,9 @@ import com.wire.kalium.logic.feature.message.ephemeral.EnqueueMessageSelfDeletio
import com.wire.kalium.logic.feature.message.getPaginatedFlowOfMessagesByConversation
import com.wire.kalium.logic.feature.message.getPaginatedFlowOfMessagesBySearchQueryAndConversation
import com.wire.kalium.logic.feature.sessionreset.ResetSessionUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped

Expand Down Expand Up @@ -151,6 +152,11 @@ class MessageModule {
fun provideGetPaginatedMessagesUseCase(messageScope: MessageScope): GetPaginatedFlowOfMessagesByConversationUseCase =
messageScope.getPaginatedFlowOfMessagesByConversation

@ViewModelScoped
@Provides
fun provideGetAssetMessagesUseCase(messageScope: MessageScope): GetAssetMessagesForConversationUseCase =
messageScope.getAssetMessagesByConversation

@ViewModelScoped
@Provides
fun provideGetPaginatedFlowOfMessagesBySearchQueryAndConversation(
Expand Down
Expand Up @@ -59,7 +59,7 @@ class RegularMessageMapper @Inject constructor(
message: Message.Regular,
sender: User?,
userList: List<User>
) = when (val content = message.content) {
): UIMessageContent = when (val content = message.content) {
is Asset -> {
when (val metadata = content.value.metadata) {
is AssetContent.AssetMetadata.Audio -> {
Expand Down
41 changes: 41 additions & 0 deletions app/src/main/kotlin/com/wire/android/mapper/UIAssetMapper.kt
@@ -0,0 +1,41 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.mapper

import com.wire.android.R
import com.wire.android.ui.home.conversations.model.messagetypes.asset.UIAssetMessage
import com.wire.android.util.ui.UIText
import com.wire.kalium.logic.data.asset.AssetMessage
import javax.inject.Inject

class UIAssetMapper @Inject constructor() {

fun toUIAsset(assetMessage: AssetMessage): UIAssetMessage {
return UIAssetMessage(
assetId = assetMessage.assetId,
time = assetMessage.time,
username = assetMessage.username?.let { UIText.DynamicString(it) }
?: UIText.StringResource(R.string.username_unavailable_label),
conversationId = assetMessage.conversationId,
messageId = assetMessage.messageId,
assetPath = assetMessage.assetPath,
downloadStatus = assetMessage.downloadStatus,
isSelfAsset = assetMessage.isSelfAsset
)
}
}
Expand Up @@ -86,6 +86,7 @@ import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar
import com.wire.android.ui.common.topappbar.WireTopAppBarTitle
import com.wire.android.ui.common.visbility.rememberVisibilityState
import com.wire.android.ui.destinations.AddMembersSearchScreenDestination
import com.wire.android.ui.destinations.ConversationMediaScreenDestination
import com.wire.android.ui.destinations.EditConversationNameScreenDestination
import com.wire.android.ui.destinations.EditGuestAccessScreenDestination
import com.wire.android.ui.destinations.EditSelfDeletingMessagesScreenDestination
Expand Down Expand Up @@ -142,6 +143,16 @@ fun GroupConversationDetailsScreen(
)
}

val onConversationMediaClick: () -> Unit = {
navigator.navigate(
NavigationCommand(
ConversationMediaScreenDestination(
conversationId = viewModel.conversationId
)
)
)
}

GroupConversationDetailsContent(
conversationSheetContent = viewModel.conversationSheetContent,
bottomSheetEventsHandler = viewModel,
Expand Down Expand Up @@ -222,7 +233,8 @@ fun GroupConversationDetailsScreen(
navigator.navigate(NavigationCommand(EditConversationNameScreenDestination(viewModel.conversationId)))
},
isLoading = viewModel.requestInProgress,
onSearchConversationMessagesClick = onSearchConversationMessagesClick
onSearchConversationMessagesClick = onSearchConversationMessagesClick,
onConversationMediaClick = onConversationMediaClick
)

val tryAgainSnackBarMessage = stringResource(id = R.string.error_unknown_message)
Expand Down Expand Up @@ -263,7 +275,8 @@ private fun GroupConversationDetailsContent(
onDeleteGroup: (GroupDialogState) -> Unit,
groupParticipantsState: GroupConversationParticipantsState,
isLoading: Boolean,
onSearchConversationMessagesClick: () -> Unit
onSearchConversationMessagesClick: () -> Unit,
onConversationMediaClick: () -> Unit
) {
val scope = rememberCoroutineScope()
val resources = LocalContext.current.resources
Expand Down Expand Up @@ -332,7 +345,8 @@ private fun GroupConversationDetailsContent(
conversationId = it.conversationId,
totalParticipants = groupParticipantsState.data.allCount,
isLoading = isLoading,
onSearchConversationMessagesClick = onSearchConversationMessagesClick
onSearchConversationMessagesClick = onSearchConversationMessagesClick,
onConversationMediaClick = onConversationMediaClick
)
}
WireTabRow(
Expand Down Expand Up @@ -539,7 +553,8 @@ fun PreviewGroupConversationDetails() {
onEditGroupName = {},
onEditSelfDeletingMessages = {},
onEditGuestAccess = {},
onSearchConversationMessagesClick = {}
onSearchConversationMessagesClick = {},
onConversationMediaClick = {}
)
}
}
Expand Up @@ -38,7 +38,7 @@ import com.wire.android.R
import com.wire.android.ui.common.colorsScheme
import com.wire.android.ui.common.conversationColor
import com.wire.android.ui.common.dimensions
import com.wire.android.ui.home.conversations.search.messages.SearchConversationMessagesButton
import com.wire.android.ui.common.spacers.VerticalSpace
import com.wire.android.ui.home.conversationslist.common.GroupConversationAvatar
import com.wire.android.ui.theme.wireColorScheme
import com.wire.android.ui.theme.wireTypography
Expand All @@ -52,6 +52,7 @@ fun GroupConversationDetailsTopBarCollapsing(
totalParticipants: Int,
isLoading: Boolean,
onSearchConversationMessagesClick: () -> Unit,
onConversationMediaClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
Expand Down Expand Up @@ -123,8 +124,10 @@ fun GroupConversationDetailsTopBarCollapsing(
}
}

SearchConversationMessagesButton(
onSearchConversationMessagesClick = onSearchConversationMessagesClick
VerticalSpace.x24()
SearchAndMediaRow(
onSearchConversationMessagesClick = onSearchConversationMessagesClick,
onConversationMediaClick = onConversationMediaClick
)
}
}
@@ -0,0 +1,45 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.ui.home.conversations.details

import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.wire.android.ui.common.dimensions
import com.wire.android.ui.common.spacers.HorizontalSpace
import com.wire.android.ui.home.conversations.media.ConversationMediaButton
import com.wire.android.ui.home.conversations.search.messages.SearchConversationMessagesButton

@Composable
fun SearchAndMediaRow(
onSearchConversationMessagesClick: () -> Unit,
onConversationMediaClick: () -> Unit
) {
Row(modifier = Modifier.padding(horizontal = dimensions().spacing16x)) {
SearchConversationMessagesButton(
modifier = Modifier.weight(1F),
onClick = onSearchConversationMessagesClick
)
HorizontalSpace.x8()
ConversationMediaButton(
modifier = Modifier.weight(1F),
onClick = onConversationMediaClick
)
}
}
@@ -0,0 +1,134 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/

package com.wire.android.ui.home.conversations.media

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.wire.android.mapper.UIAssetMapper
import com.wire.android.navigation.SavedStateViewModel
import com.wire.android.ui.home.conversations.ConversationNavArgs
import com.wire.android.ui.navArgs
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.message.Message
import com.wire.kalium.logic.feature.asset.GetAssetMessagesForConversationUseCase
import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase
import com.wire.kalium.logic.feature.asset.MessageAssetResult
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject

@HiltViewModel
@Suppress("LongParameterList", "TooManyFunctions")
class ConversationAssetMessagesViewModel @Inject constructor(
override val savedStateHandle: SavedStateHandle,
private val dispatchers: DispatcherProvider,
private val getAssets: GetAssetMessagesForConversationUseCase,
private val getPrivateAsset: GetMessageAssetUseCase,
private val assetMapper: UIAssetMapper,
) : SavedStateViewModel(savedStateHandle) {

private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs()
val conversationId: QualifiedID = conversationNavArgs.conversationId

var viewState by mutableStateOf(ConversationAssetMessagesViewState())
private set

private var continueLoading = true
private var isLoading = false
private var currentOffset: Int = 0

init {
loadAssets()
}

fun continueLoading(shouldContinue: Boolean) {
if (shouldContinue) {
if (!continueLoading) {
continueLoading = true
loadAssets()
}
} else {
continueLoading = false
}
}

private fun loadAssets() = viewModelScope.launch {
if (isLoading) {
return@launch
}
isLoading = true
try {
while (continueLoading) {
val uiAssetList = withContext(dispatchers.io()) {
getAssets.invoke(
conversationId = conversationId,
limit = BATCH_SIZE,
offset = currentOffset
).map(assetMapper::toUIAsset)
}

// imitate loading new asset batch
viewState = viewState.copy(messages = viewState.messages.plus(uiAssetList.map {
it.copy(
downloadStatus = if (it.assetPath == null && it.downloadStatus != Message.DownloadStatus.FAILED_DOWNLOAD) {
Message.DownloadStatus.DOWNLOAD_IN_PROGRESS
} else {
it.downloadStatus
}
)
}).toImmutableList())

if (uiAssetList.size >= BATCH_SIZE) {
val uiMessages = uiAssetList.map { uiAsset ->
if (uiAsset.assetPath == null) {
val assetPath = withContext(dispatchers.io()) {
when (val asset = getPrivateAsset.invoke(uiAsset.conversationId, uiAsset.messageId).await()) {
is MessageAssetResult.Failure -> null
is MessageAssetResult.Success -> asset.decodedAssetPath
}
}
uiAsset.copy(assetPath = assetPath)
} else {
uiAsset
}
}
currentOffset += BATCH_SIZE

viewState = viewState.copy(
messages = viewState.messages.dropLast(uiMessages.size).plus(uiMessages).toImmutableList(),
)
} else {
continueLoading = false
}
}
} finally {
isLoading = false
}
}

companion object {
const val BATCH_SIZE = 5
}
}
@@ -0,0 +1,31 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*
*/

package com.wire.android.ui.home.conversations.media

import androidx.compose.runtime.Stable
import com.wire.android.ui.home.conversations.model.messagetypes.asset.UIAssetMessage
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf

@Stable
data class ConversationAssetMessagesViewState(
val messages: ImmutableList<UIAssetMessage> = persistentListOf()
)

0 comments on commit fc6e59e

Please sign in to comment.