Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: media gallery [WPB-4989] #2490

Merged
merged 16 commits into from
Dec 4, 2023
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
)
}
}
Original file line number Diff line number Diff line change
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 = {}
)
}
}
Original file line number Diff line number Diff line change
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
)
}
}
Original file line number Diff line number Diff line change
@@ -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
)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
saleniuk marked this conversation as resolved.
Show resolved Hide resolved

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
saleniuk marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -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()
)