Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ package com.wire.android.di.accountScoped

import com.wire.android.di.CurrentAccount
import com.wire.android.di.KaliumCoreLogic
import com.wire.android.feature.cells.ui.versioning.VersionGroupHelper
import com.wire.android.feature.cells.util.FileNameResolver
import com.wire.android.ui.home.conversations.model.messagetypes.multipart.CellAssetRefreshHelper
import com.wire.android.util.FileSizeFormatter
import com.wire.kalium.cells.CellsScope
import com.wire.kalium.cells.domain.CellUploadManager
import com.wire.kalium.cells.domain.usecase.AddAttachmentDraftUseCase
Expand Down Expand Up @@ -220,4 +222,8 @@ class CellsModule {
@Provides
fun provideRestoreNodeVersionUseCase(cellsScope: CellsScope): RestoreNodeVersionUseCase =
cellsScope.restoreNodeVersion

@ViewModelScoped
@Provides
fun provideVersionGroupHelper(fileSizeFormatter: FileSizeFormatter): VersionGroupHelper = VersionGroupHelper(fileSizeFormatter)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Wire
* Copyright (C) 2025 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.feature.cells.ui.versioning

import com.wire.android.feature.cells.R
import com.wire.android.feature.cells.ui.versioning.VersionHistoryViewModel.Companion.DATE_PATTERN
import com.wire.android.util.FileSizeFormatter
import com.wire.android.util.ui.UIText
import com.wire.kalium.cells.domain.model.NodeVersion
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import kotlin.time.Clock
import kotlin.time.ExperimentalTime

class VersionGroupHelper @OptIn(ExperimentalTime::class) constructor(
private val fileSizeFormatter: FileSizeFormatter,
private val currentTime: () -> Long = { Clock.System.now().toEpochMilliseconds() }
) {

fun groupByDay(versions: List<NodeVersion>): List<VersionGroup> {
val today = LocalDate.ofInstant(Instant.ofEpochMilli(currentTime()), ZoneId.systemDefault())
val yesterday = today.minusDays(1)

val grouped = versions.groupBy { item ->
Instant.ofEpochSecond(item.modifiedTime?.toLong() ?: 0L)
.atZone(ZoneId.systemDefault())
.toLocalDate()
}

return grouped.entries
.sortedByDescending { it.key }
.mapIndexed { groupIndex, (date, items) ->
val dateFormat = DateTimeFormatter.ofPattern(DATE_PATTERN)
val formattedDate = date.format(dateFormat)

val dateLabel: UIText = when (date) {
today -> UIText.StringResource(
R.string.date_label_today,
formattedDate
)

yesterday -> UIText.StringResource(
R.string.date_label_yesterday,
formattedDate
)

else -> UIText.DynamicString(formattedDate)
}

val uiItems = items.mapIndexed { itemIndex, apiItem ->

val formattedTime = Instant.ofEpochSecond(apiItem.modifiedTime?.toLong() ?: 0L)
.atZone(ZoneId.systemDefault())
.toLocalTime()
.format(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT))

CellVersion(
versionId = apiItem.id,
modifiedBy = apiItem.ownerName ?: "",
fileSize = fileSizeFormatter.formatSize(apiItem.size?.toLong() ?: 0),
modifiedAt = formattedTime,
isCurrentVersion = groupIndex == 0 && itemIndex == 0
)
}
VersionGroup(dateLabel, uiItems)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,33 +22,24 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.feature.cells.R
import com.wire.android.feature.cells.ui.navArgs
import com.wire.android.feature.cells.ui.versioning.restore.RestoreDialogState
import com.wire.android.feature.cells.ui.versioning.restore.RestoreVersionState
import com.wire.android.util.FileSizeFormatter
import com.wire.android.util.ui.UIText
import com.wire.kalium.cells.domain.model.NodeVersion
import com.wire.kalium.cells.domain.usecase.versioning.GetNodeVersionsUseCase
import com.wire.kalium.cells.domain.usecase.versioning.RestoreNodeVersionUseCase
import com.wire.kalium.common.functional.onFailure
import com.wire.kalium.common.functional.onSuccess
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import javax.inject.Inject

@HiltViewModel
class VersionHistoryViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val getNodeVersionsUseCase: GetNodeVersionsUseCase,
private val fileSizeFormatter: FileSizeFormatter,
private val restoreNodeVersionUseCase: RestoreNodeVersionUseCase,
private val versionGroupHelper: VersionGroupHelper,
) : ViewModel() {

private val navArgs: VersionHistoryNavArgs = savedStateHandle.navArgs()
Expand Down Expand Up @@ -81,63 +72,14 @@ class VersionHistoryViewModel @Inject constructor(
getNodeVersionsUseCase(navArgs.uuid)
.onSuccess {
versionHistoryState.value = VersionHistoryState.Success
versionsGroupedByTime.value = it.groupByDay()
versionsGroupedByTime.value = versionGroupHelper.groupByDay(it)
}
// TODO: Handle error on UI
.onFailure {
versionHistoryState.value = VersionHistoryState.Failed
}
}

private fun List<NodeVersion>.groupByDay(): List<VersionGroup> {
val today = LocalDate.now()
val yesterday = today.minusDays(1)

val grouped = this.groupBy { item ->
Instant.ofEpochSecond(item.modifiedTime?.toLong() ?: 0L)
.atZone(ZoneId.systemDefault())
.toLocalDate()
}

return grouped.entries
.sortedByDescending { it.key }
.mapIndexed { groupIndex, (date, items) ->
val dateFormat = DateTimeFormatter.ofPattern(DATE_PATTERN)
val formattedDate = date.format(dateFormat)

val dateLabel: UIText = when (date) {
today -> UIText.StringResource(
R.string.date_label_today,
formattedDate
)

yesterday -> UIText.StringResource(
R.string.date_label_yesterday,
formattedDate
)

else -> UIText.DynamicString(formattedDate)
}

val uiItems = items.mapIndexed { itemIndex, apiItem ->

val formattedTime = Instant.ofEpochSecond(apiItem.modifiedTime?.toLong() ?: 0L)
.atZone(ZoneId.systemDefault())
.toLocalTime()
.format(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT))

CellVersion(
versionId = apiItem.id,
modifiedBy = apiItem.ownerName ?: "",
fileSize = fileSizeFormatter.formatSize(apiItem.size?.toLong() ?: 0),
modifiedAt = formattedTime,
isCurrentVersion = groupIndex == 0 && itemIndex == 0
)
}
VersionGroup(dateLabel, uiItems)
}
}

// TODO: Unit test coming in another PR
fun showRestoreConfirmationDialog(versionId: String) {
restoreDialogState.value = restoreDialogState.value.copy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.currentTime
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
Expand All @@ -42,9 +43,12 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle

@ExperimentalCoroutinesApi
class VersionHistoryViewModelTest {
Expand Down Expand Up @@ -75,7 +79,8 @@ class VersionHistoryViewModelTest {
fun givenViewModel_whenItInits_thenIsFetchingStateIsManagedCorrectly() = runTest {
coEvery { getNodeVersionsUseCase(testNodeUuid) } returns Either.Right(emptyList())

val viewModel = VersionHistoryViewModel(savedStateHandle, getNodeVersionsUseCase, fileSizeFormatter, restoreNodeVersionUseCase)
val versionGroupHelper = VersionGroupHelper(fileSizeFormatter, { currentTime })
val viewModel = VersionHistoryViewModel(savedStateHandle, getNodeVersionsUseCase, restoreNodeVersionUseCase, versionGroupHelper)

assertEquals(VersionHistoryState.Idle, viewModel.versionHistoryState.value)
advanceUntilIdle()
Expand All @@ -85,7 +90,8 @@ class VersionHistoryViewModelTest {
@Suppress("LongMethod")
@Test
fun givenSuccessfulFetch_whenViewModelInits_thenVersionsAreGroupedCorrectly() = runTest {
val today = LocalDate.now()

val today = LocalDate.ofInstant(Instant.ofEpochSecond(currentTime), ZoneId.systemDefault())
val yesterday = today.minusDays(1)
val twoDaysAgo = today.minusDays(2)
val versionNode = NodeVersion(
Expand Down Expand Up @@ -128,7 +134,14 @@ class VersionHistoryViewModelTest {
coEvery { getNodeVersionsUseCase(testNodeUuid) } returns Either.Right(versionsFromApi)
every { fileSizeFormatter.formatSize(any()) } returns "30 MB"

val viewModel = VersionHistoryViewModel(savedStateHandle, getNodeVersionsUseCase, fileSizeFormatter, restoreNodeVersionUseCase)
val versionGroupHelper = VersionGroupHelper(fileSizeFormatter, { currentTime })

val viewModel = VersionHistoryViewModel(
savedStateHandle = savedStateHandle,
getNodeVersionsUseCase = getNodeVersionsUseCase,
restoreNodeVersionUseCase = restoreNodeVersionUseCase,
versionGroupHelper = versionGroupHelper,
)
testDispatcher.scheduler.advanceUntilIdle()

// Versions should be grouped into three sections (Today, Yesterday, and an older date)
Expand All @@ -145,7 +158,13 @@ class VersionHistoryViewModelTest {
assertEquals("Today, $todayFormattedDate", actualTodayText)
assertEquals(1, groupedVersions[0].versions.size)
assertEquals("User A", groupedVersions[0].versions[0].modifiedBy)
assertEquals("11:30 AM", groupedVersions[0].versions[0].modifiedAt)

val expectedTime = Instant
.ofEpochSecond(versionNode.modifiedTime!!.toLong())
.atZone(ZoneId.systemDefault())
.toLocalTime()
.format(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT))
assertEquals(expectedTime, groupedVersions[0].versions[0].modifiedAt)

// Verify "Yesterday" group is correct
every { fileSizeFormatter.formatSize(any()) } returns groupedVersions[1].versions[0].fileSize
Expand All @@ -168,7 +187,8 @@ class VersionHistoryViewModelTest {
fun givenApiFailure_whenViewModelInits_thenVersionListIsEmpty() = runTest {
coEvery { getNodeVersionsUseCase(testNodeUuid) } returns Either.Left(CoreFailure.MissingClientRegistration)

val viewModel = VersionHistoryViewModel(savedStateHandle, getNodeVersionsUseCase, fileSizeFormatter, restoreNodeVersionUseCase)
val versionGroupHelper = VersionGroupHelper(fileSizeFormatter, { currentTime })
val viewModel = VersionHistoryViewModel(savedStateHandle, getNodeVersionsUseCase, restoreNodeVersionUseCase, versionGroupHelper)
advanceUntilIdle()

assertTrue(viewModel.versionsGroupedByTime.value.isEmpty())
Expand All @@ -179,7 +199,8 @@ class VersionHistoryViewModelTest {
fun givenMissingUuid_whenViewModelInits_thenNoFetchIsAttempted() = runTest {
every { savedStateHandle.get<String>("uuid") } returns null

val viewModel = VersionHistoryViewModel(savedStateHandle, getNodeVersionsUseCase, fileSizeFormatter, restoreNodeVersionUseCase)
val versionGroupHelper = VersionGroupHelper(fileSizeFormatter, { currentTime })
val viewModel = VersionHistoryViewModel(savedStateHandle, getNodeVersionsUseCase, restoreNodeVersionUseCase, versionGroupHelper)
advanceUntilIdle()

assertTrue(viewModel.versionsGroupedByTime.value.isEmpty())
Expand Down