diff --git a/core/persistence/src/androidUnitTest/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/DefaultLongToLongMapSerializerTest.kt b/core/persistence/src/androidUnitTest/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/DefaultLongToLongMapSerializerTest.kt new file mode 100644 index 000000000..8093ef469 --- /dev/null +++ b/core/persistence/src/androidUnitTest/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/DefaultLongToLongMapSerializerTest.kt @@ -0,0 +1,44 @@ +package com.livefast.eattrash.raccoonforlemmy.core.persistence.repository + +import kotlin.test.Test +import kotlin.test.assertEquals + +class DefaultLongToLongMapSerializerTest { + private val sut = DefaultLongToLongMapSerializer() + + @Test + fun givenEmpty_whenSerializeMap_thenResultIsAsExpected() { + val map = mapOf() + + val res = sut.serializeMap(map) + + assertEquals(emptyList(), res) + } + + @Test + fun whenSerializeMap_thenResultIsAsExpected() { + val map = mapOf(1L to 2L) + + val res = sut.serializeMap(map) + + assertEquals(listOf("1:2"), res) + } + + @Test + fun givenEmpty_whenDeserializeMap_thenResultIsAsExpected() { + val list = listOf() + + val res = sut.deserializeMap(list) + + assertEquals(mapOf(), res) + } + + @Test + fun whenDeserializeMap_thenResultIsAsExpected() { + val list = listOf("1:2") + + val res = sut.deserializeMap(list) + + assertEquals(mapOf(1L to 2L), res) + } +} diff --git a/core/persistence/src/androidUnitTest/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/DefaultPostLastSeenDateRepositoryTest.kt b/core/persistence/src/androidUnitTest/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/DefaultPostLastSeenDateRepositoryTest.kt new file mode 100644 index 000000000..0e4d83866 --- /dev/null +++ b/core/persistence/src/androidUnitTest/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/DefaultPostLastSeenDateRepositoryTest.kt @@ -0,0 +1,110 @@ +package com.livefast.eattrash.raccoonforlemmy.core.persistence.repository + +import com.livefast.eattrash.raccoonforlemmy.core.preferences.store.TemporaryKeyStore +import com.livefast.eattrash.raccoonforlemmy.core.testutils.DispatcherTestRule +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import junit.framework.TestCase.assertNull +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import kotlin.test.Test +import kotlin.test.assertEquals + +class DefaultPostLastSeenDateRepositoryTest { + @get:Rule + val dispatcherTestRule = DispatcherTestRule() + + private val keyStore = mockk(relaxUnitFun = true) + private val serializer = + mockk { + every { deserializeMap(any()) } answers { + firstArg>() + .associate { s -> + val tokens = s.split(":").map { it.trim() } + tokens[0].toLong() to tokens[1].toLong() + }.toMutableMap() + } + every { serializeMap(any()) } answers { + firstArg>() + .map { entry -> + "${entry.key}:${entry.value}" + }.toList() + } + } + + private val sut = + DefaultPostLastSeenDateRepository( + keyStore = keyStore, + serializer = serializer, + ) + + @Test + fun givenEmptyInitialState_whenSave_thenValueIsStored() = + runTest { + every { keyStore.get(KEY, listOf()) } returns listOf() + sut.save(1L, 2L) + + verify { + keyStore.save(KEY, listOf("1:2")) + } + } + + @Test + fun givenEntryAlreadyExisting_whenSave_thenValueIsStored() = + runTest { + every { keyStore.get(KEY, listOf()) } returns listOf("1:2") + + sut.save(1, 3) + + verify { + keyStore.save(KEY, listOf("1:3")) + } + } + + @Test + fun givenOtherEntryAlreadyExisting_whenSave_thenBothValuesAreStored() = + runTest { + every { keyStore.get(KEY, listOf()) } returns listOf("1:2") + + sut.save(3, 4) + + verify { + keyStore.save(KEY, listOf("1:2", "3:4")) + } + } + + @Test + fun givenEmptyInitialState_whenGet_thenResultIsAsExpected() = + runTest { + every { keyStore.get(KEY, listOf()) } returns listOf() + + val res = sut.get(1L) + + assertNull(res) + } + + @Test + fun givenCommunityExists_whenGet_thenResultIsAsExpected() = + runTest { + every { keyStore.get(KEY, listOf()) } returns listOf("1:2") + + val res = sut.get(1L) + + assertEquals(2, res) + } + + @Test + fun givenCommunityDoesNotExist_whenGet_thenResultIsAsExpected() = + runTest { + every { keyStore.get(KEY, listOf()) } returns listOf("1:2") + + val res = sut.get(3L) + + assertNull(res) + } + + companion object { + private const val KEY = "postLastSeenDate" + } +} diff --git a/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/di/PersistenceModule.kt b/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/di/PersistenceModule.kt index 115d94d09..0fe87a335 100644 --- a/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/di/PersistenceModule.kt +++ b/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/di/PersistenceModule.kt @@ -12,7 +12,9 @@ import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.Default import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.DefaultDraftRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.DefaultFavoriteCommunityRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.DefaultInstanceSelectionRepository +import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.DefaultLongToLongMapSerializer import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.DefaultMultiCommunityRepository +import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.DefaultPostLastSeenDateRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.DefaultSettingsRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.DefaultSortSerializer import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.DefaultStopWordRepository @@ -22,7 +24,9 @@ import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.DomainB import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.DraftRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.FavoriteCommunityRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.InstanceSelectionRepository +import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.LongToLongMapSerializer import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.MultiCommunityRepository +import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.PostLastSeenDateRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.SettingsRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.SortSerializer import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.StopWordRepository @@ -164,4 +168,17 @@ val persistenceModule = ) } } + bind { + singleton { + DefaultLongToLongMapSerializer() + } + } + bind { + singleton { + DefaultPostLastSeenDateRepository( + keyStore = instance(), + serializer = instance(), + ) + } + } } diff --git a/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/DefaultLongToLongMapSerializer.kt b/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/DefaultLongToLongMapSerializer.kt new file mode 100644 index 000000000..26b6f4ae9 --- /dev/null +++ b/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/DefaultLongToLongMapSerializer.kt @@ -0,0 +1,25 @@ +package com.livefast.eattrash.raccoonforlemmy.core.persistence.repository + +internal class DefaultLongToLongMapSerializer : LongToLongMapSerializer { + override fun deserializeMap(list: List): MutableMap = + list + .mapNotNull { + it.split(":").takeIf { e -> e.size == 2 }?.let { e -> e[0] to e[1] } + }.let { pairs -> + val res = mutableMapOf() + for (pair in pairs) { + res[pair.first.toLong()] = pair.second.toLong() + } + res + } + + override fun serializeMap(map: Map): List = + map.map { e -> + buildString { + append("") + append(e.key) + append(":") + append(e.value) + } + } +} diff --git a/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/DefaultPostLastSeenDateRepository.kt b/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/DefaultPostLastSeenDateRepository.kt new file mode 100644 index 000000000..83726fb61 --- /dev/null +++ b/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/DefaultPostLastSeenDateRepository.kt @@ -0,0 +1,42 @@ +package com.livefast.eattrash.raccoonforlemmy.core.persistence.repository + +import com.livefast.eattrash.raccoonforlemmy.core.preferences.store.TemporaryKeyStore +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.withContext + +internal class DefaultPostLastSeenDateRepository( + private val keyStore: TemporaryKeyStore, + private val serializer: LongToLongMapSerializer, +) : PostLastSeenDateRepository { + override suspend fun get(postId: Long): Long? = + withContext(Dispatchers.IO) { + val map = + keyStore.get(SETTINGS_KEY, listOf()).let { + serializer.deserializeMap(it) + } + map[postId] + } + + override suspend fun save( + postId: Long, + timestamp: Long, + ) = withContext(Dispatchers.IO) { + val map = + keyStore.get(SETTINGS_KEY, listOf()).let { + serializer.deserializeMap(it) + } + map[postId] = timestamp + val newValue = serializer.serializeMap(map) + keyStore.save(SETTINGS_KEY, newValue) + } + + override suspend fun clear() = + withContext(Dispatchers.IO) { + keyStore.remove(SETTINGS_KEY) + } + + companion object { + private const val SETTINGS_KEY = "postLastSeenDate" + } +} diff --git a/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/DefaultSortSerializer.kt b/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/DefaultSortSerializer.kt index 458bebc33..e41bd1932 100644 --- a/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/DefaultSortSerializer.kt +++ b/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/DefaultSortSerializer.kt @@ -15,6 +15,10 @@ internal class DefaultSortSerializer : SortSerializer { override fun serializeMap(map: Map): List = map.map { e -> - e.key + ":" + e.value + buildString { + append(e.key) + append(":") + append(e.value) + } } } diff --git a/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/LongToLongMapSerializer.kt b/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/LongToLongMapSerializer.kt new file mode 100644 index 000000000..1e95356fd --- /dev/null +++ b/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/LongToLongMapSerializer.kt @@ -0,0 +1,7 @@ +package com.livefast.eattrash.raccoonforlemmy.core.persistence.repository + +internal interface LongToLongMapSerializer { + fun deserializeMap(list: List): MutableMap + + fun serializeMap(map: Map): List +} diff --git a/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/PostLastSeenDateRepository.kt b/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/PostLastSeenDateRepository.kt new file mode 100644 index 000000000..4e3c25063 --- /dev/null +++ b/core/persistence/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/persistence/repository/PostLastSeenDateRepository.kt @@ -0,0 +1,12 @@ +package com.livefast.eattrash.raccoonforlemmy.core.persistence.repository + +interface PostLastSeenDateRepository { + suspend fun get(postId: Long): Long? + + suspend fun save( + postId: Long, + timestamp: Long, + ) + + suspend fun clear() +} diff --git a/core/utils/src/androidMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/utils/datetime/DateFunctions.kt b/core/utils/src/androidMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/utils/datetime/DateFunctions.kt index 3bae75e4a..162a3e802 100644 --- a/core/utils/src/androidMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/utils/datetime/DateFunctions.kt +++ b/core/utils/src/androidMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/utils/datetime/DateFunctions.kt @@ -25,6 +25,11 @@ actual fun Long.toIso8601Timestamp(): String? { return safeFormatter.format(date) } +actual fun String.toTimestamp(): Long { + val date = getDateFromIso8601Timestamp(this) + return date.toInstant().toEpochMilli() +} + actual fun getFormattedDate( iso8601Timestamp: String, format: String, diff --git a/core/utils/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/utils/datetime/DateFunctions.kt b/core/utils/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/utils/datetime/DateFunctions.kt index 59a2807d2..22cc72def 100644 --- a/core/utils/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/utils/datetime/DateFunctions.kt +++ b/core/utils/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/utils/datetime/DateFunctions.kt @@ -4,6 +4,8 @@ expect fun epochMillis(): Long expect fun Long.toIso8601Timestamp(): String? +expect fun String.toTimestamp(): Long + expect fun getFormattedDate( iso8601Timestamp: String, format: String, diff --git a/core/utils/src/iosMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/utils/datetime/DateFunctions.kt b/core/utils/src/iosMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/utils/datetime/DateFunctions.kt index a1db6e851..1518d4420 100644 --- a/core/utils/src/iosMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/utils/datetime/DateFunctions.kt +++ b/core/utils/src/iosMain/kotlin/com/livefast/eattrash/raccoonforlemmy/core/utils/datetime/DateFunctions.kt @@ -16,6 +16,7 @@ import platform.Foundation.NSTimeZone import platform.Foundation.autoupdatingCurrentLocale import platform.Foundation.localTimeZone import platform.Foundation.timeIntervalSince1970 +import kotlin.math.roundToLong actual fun epochMillis(): Long = (NSDate().timeIntervalSince1970 * 1000).toLong() @@ -28,6 +29,11 @@ actual fun Long.toIso8601Timestamp(): String? { return dateFormatter.stringFromDate(date) } +actual fun String.toTimestamp(): Long { + val date = getDateFromIso8601Timestamp(this) + return date?.timeIntervalSince1970?.let { (it * 1000).roundToLong() } ?: 0 +} + actual fun getFormattedDate( iso8601Timestamp: String, format: String, diff --git a/domain/identity/src/androidUnitTest/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultLoginUseCaseTest.kt b/domain/identity/src/androidUnitTest/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultLoginUseCaseTest.kt index 98671f77f..14ec1b8ab 100644 --- a/domain/identity/src/androidUnitTest/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultLoginUseCaseTest.kt +++ b/domain/identity/src/androidUnitTest/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultLoginUseCaseTest.kt @@ -7,7 +7,9 @@ import com.livefast.eattrash.raccoonforlemmy.core.persistence.data.SettingsModel import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.AccountRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.CommunityPreferredLanguageRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.CommunitySortRepository +import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.PostLastSeenDateRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.SettingsRepository +import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.UserSortRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.usecase.CreateSpecialTagsUseCase import com.livefast.eattrash.raccoonforlemmy.core.testutils.DispatcherTestRule import com.livefast.eattrash.raccoonforlemmy.domain.identity.repository.ApiConfigurationRepository @@ -42,6 +44,8 @@ class DefaultLoginUseCaseTest { private val communitySortRepository = mockk(relaxUnitFun = true) private val communityPreferredLanguageRepository = mockk(relaxUnitFun = true) + private val userSortRepository = mockk(relaxUnitFun = true) + private val postLastSeenDateRepository = mockk(relaxUnitFun = true) private val bottomNavItemsRepository = mockk(relaxUnitFun = true) { coEvery { get(accountId = any()) } returns BottomNavItemsRepository.DEFAULT_ITEMS @@ -61,6 +65,8 @@ class DefaultLoginUseCaseTest { bottomNavItemsRepository = bottomNavItemsRepository, lemmyValueCache = lemmyValueCache, createSpecialTagsUseCase = createSpecialTagsUseCase, + userSortRepository = userSortRepository, + postLastSeenDateRepository = postLastSeenDateRepository, ) @Test @@ -104,6 +110,8 @@ class DefaultLoginUseCaseTest { settingsRepository.createSettings(anonymousSettings, accountId) settingsRepository.changeCurrentSettings(anonymousSettings) communitySortRepository.clear() + userSortRepository.clear() + postLastSeenDateRepository.clear() } } @@ -156,6 +164,8 @@ class DefaultLoginUseCaseTest { settingsRepository.createSettings(anonymousSettings, accountId) settingsRepository.changeCurrentSettings(anonymousSettings) communitySortRepository.clear() + userSortRepository.clear() + postLastSeenDateRepository.clear() } } @@ -201,6 +211,8 @@ class DefaultLoginUseCaseTest { settingsRepository.changeCurrentSettings(oldSettings) communitySortRepository.clear() communityPreferredLanguageRepository.clear() + userSortRepository.clear() + postLastSeenDateRepository.clear() } coVerify(inverse = true) { accountRepository.createAccount(any()) diff --git a/domain/identity/src/androidUnitTest/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultLogoutUseCaseTest.kt b/domain/identity/src/androidUnitTest/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultLogoutUseCaseTest.kt index 9b66c3351..229b0c8ba 100644 --- a/domain/identity/src/androidUnitTest/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultLogoutUseCaseTest.kt +++ b/domain/identity/src/androidUnitTest/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultLogoutUseCaseTest.kt @@ -7,7 +7,9 @@ import com.livefast.eattrash.raccoonforlemmy.core.persistence.data.AccountModel import com.livefast.eattrash.raccoonforlemmy.core.persistence.data.SettingsModel import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.AccountRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.CommunitySortRepository +import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.PostLastSeenDateRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.SettingsRepository +import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.UserSortRepository import com.livefast.eattrash.raccoonforlemmy.core.testutils.DispatcherTestRule import com.livefast.eattrash.raccoonforlemmy.domain.identity.repository.IdentityRepository import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.repository.LemmyValueCache @@ -29,6 +31,8 @@ class DefaultLogoutUseCaseTest { private val notificationCenter = mockk(relaxUnitFun = true) private val communitySortRepository = mockk(relaxUnitFun = true) private val lemmyValueCache = mockk(relaxUnitFun = true) + private val userSortRepository = mockk(relaxUnitFun = true) + private val postLastSeenDateRepository = mockk(relaxUnitFun = true) private val bottomNavItemsRepository = mockk(relaxUnitFun = true) { coEvery { get(accountId = any()) } returns BottomNavItemsRepository.DEFAULT_ITEMS @@ -44,6 +48,8 @@ class DefaultLogoutUseCaseTest { bottomNavItemsRepository = bottomNavItemsRepository, lemmyValueCache = lemmyValueCache, userTagHelper = userTagHelper, + userSortRepository = userSortRepository, + postLastSeenDateRepository = postLastSeenDateRepository, ) @Test @@ -74,6 +80,8 @@ class DefaultLogoutUseCaseTest { accountRepository.setActive(accountId, false) settingsRepository.changeCurrentSettings(anonymousSettings) userTagHelper.clear() + userSortRepository.clear() + postLastSeenDateRepository.clear() } } } diff --git a/domain/identity/src/androidUnitTest/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultSwitchAccountUseCaseTest.kt b/domain/identity/src/androidUnitTest/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultSwitchAccountUseCaseTest.kt index 1685dc38a..b308da83e 100644 --- a/domain/identity/src/androidUnitTest/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultSwitchAccountUseCaseTest.kt +++ b/domain/identity/src/androidUnitTest/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultSwitchAccountUseCaseTest.kt @@ -9,7 +9,9 @@ import com.livefast.eattrash.raccoonforlemmy.core.persistence.data.SettingsModel import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.AccountRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.CommunityPreferredLanguageRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.CommunitySortRepository +import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.PostLastSeenDateRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.SettingsRepository +import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.UserSortRepository import com.livefast.eattrash.raccoonforlemmy.core.testutils.DispatcherTestRule import com.livefast.eattrash.raccoonforlemmy.domain.identity.repository.IdentityRepository import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.repository.LemmyValueCache @@ -33,6 +35,8 @@ class DefaultSwitchAccountUseCaseTest { private val communitySortRepository = mockk(relaxUnitFun = true) private val communityPreferredLanguageRepository = mockk(relaxUnitFun = true) + private val userSortRepository = mockk(relaxUnitFun = true) + private val postLastSeenDateRepository = mockk(relaxUnitFun = true) private val lemmyValueCache = mockk(relaxUnitFun = true) private val bottomNavItemsRepository = mockk(relaxUnitFun = true) { @@ -51,6 +55,8 @@ class DefaultSwitchAccountUseCaseTest { bottomNavItemsRepository = bottomNavItemsRepository, lemmyValueCache = lemmyValueCache, userTagHelper = userTagHelper, + userSortRepository = userSortRepository, + postLastSeenDateRepository = postLastSeenDateRepository, ) @Test @@ -101,6 +107,8 @@ class DefaultSwitchAccountUseCaseTest { identityRepository.refreshLoggedState() serviceProvider.changeInstance("new-instance") userTagHelper.clear() + userSortRepository.clear() + postLastSeenDateRepository.clear() } } } diff --git a/domain/identity/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/di/IdentityModule.kt b/domain/identity/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/di/IdentityModule.kt index 4ac1eed40..47180f8fc 100644 --- a/domain/identity/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/di/IdentityModule.kt +++ b/domain/identity/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/di/IdentityModule.kt @@ -139,6 +139,8 @@ val identityModule = bottomNavItemsRepository = instance(), lemmyValueCache = instance(), createSpecialTagsUseCase = instance(), + userSortRepository = instance(), + postLastSeenDateRepository = instance(), ) } } @@ -153,6 +155,8 @@ val identityModule = bottomNavItemsRepository = instance(), userTagHelper = instance(), lemmyValueCache = instance(), + userSortRepository = instance(), + postLastSeenDateRepository = instance(), ) } } @@ -169,6 +173,8 @@ val identityModule = bottomNavItemsRepository = instance(), userTagHelper = instance(), lemmyValueCache = instance(), + userSortRepository = instance(), + postLastSeenDateRepository = instance(), ) } } diff --git a/domain/identity/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultLoginUseCase.kt b/domain/identity/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultLoginUseCase.kt index 4d8c57561..dc0c844ce 100644 --- a/domain/identity/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultLoginUseCase.kt +++ b/domain/identity/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultLoginUseCase.kt @@ -7,7 +7,9 @@ import com.livefast.eattrash.raccoonforlemmy.core.persistence.data.AccountModel import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.AccountRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.CommunityPreferredLanguageRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.CommunitySortRepository +import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.PostLastSeenDateRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.SettingsRepository +import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.UserSortRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.usecase.CreateSpecialTagsUseCase import com.livefast.eattrash.raccoonforlemmy.domain.identity.repository.ApiConfigurationRepository import com.livefast.eattrash.raccoonforlemmy.domain.identity.repository.AuthRepository @@ -27,6 +29,8 @@ internal class DefaultLoginUseCase( private val bottomNavItemsRepository: BottomNavItemsRepository, private val lemmyValueCache: LemmyValueCache, private val createSpecialTagsUseCase: CreateSpecialTagsUseCase, + private val userSortRepository: UserSortRepository, + private val postLastSeenDateRepository: PostLastSeenDateRepository, ) : LoginUseCase { override suspend operator fun invoke( instance: String, @@ -108,6 +112,8 @@ internal class DefaultLoginUseCase( communitySortRepository.clear() communityPreferredLanguageRepository.clear() + userSortRepository.clear() + postLastSeenDateRepository.clear() val newSettings = settingsRepository.getSettings(accountId) settingsRepository.changeCurrentSettings(newSettings) diff --git a/domain/identity/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultLogoutUseCase.kt b/domain/identity/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultLogoutUseCase.kt index 4b638af04..f583bd4a0 100644 --- a/domain/identity/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultLogoutUseCase.kt +++ b/domain/identity/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultLogoutUseCase.kt @@ -6,7 +6,9 @@ import com.livefast.eattrash.raccoonforlemmy.core.notifications.NotificationCent import com.livefast.eattrash.raccoonforlemmy.core.notifications.NotificationCenterEvent import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.AccountRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.CommunitySortRepository +import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.PostLastSeenDateRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.SettingsRepository +import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.UserSortRepository import com.livefast.eattrash.raccoonforlemmy.domain.identity.repository.IdentityRepository import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.repository.LemmyValueCache import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.repository.UserTagHelper @@ -20,6 +22,8 @@ internal class DefaultLogoutUseCase( private val bottomNavItemsRepository: BottomNavItemsRepository, private val lemmyValueCache: LemmyValueCache, private val userTagHelper: UserTagHelper, + private val userSortRepository: UserSortRepository, + private val postLastSeenDateRepository: PostLastSeenDateRepository, ) : LogoutUseCase { override suspend operator fun invoke() { notificationCenter.send(NotificationCenterEvent.ResetExplore) @@ -27,6 +31,8 @@ internal class DefaultLogoutUseCase( identityRepository.clearToken() communitySortRepository.clear() + userSortRepository.clear() + postLastSeenDateRepository.clear() lemmyValueCache.refresh() notificationCenter.send(NotificationCenterEvent.Logout) diff --git a/domain/identity/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultSwitchAccountUseCase.kt b/domain/identity/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultSwitchAccountUseCase.kt index c8b8e100f..e3d3a13b3 100644 --- a/domain/identity/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultSwitchAccountUseCase.kt +++ b/domain/identity/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/domain/identity/usecase/DefaultSwitchAccountUseCase.kt @@ -9,7 +9,9 @@ import com.livefast.eattrash.raccoonforlemmy.core.persistence.data.AccountModel import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.AccountRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.CommunityPreferredLanguageRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.CommunitySortRepository +import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.PostLastSeenDateRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.SettingsRepository +import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.UserSortRepository import com.livefast.eattrash.raccoonforlemmy.domain.identity.repository.IdentityRepository import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.repository.LemmyValueCache import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.repository.UserTagHelper @@ -25,6 +27,8 @@ internal class DefaultSwitchAccountUseCase( private val bottomNavItemsRepository: BottomNavItemsRepository, private val lemmyValueCache: LemmyValueCache, private val userTagHelper: UserTagHelper, + private val userSortRepository: UserSortRepository, + private val postLastSeenDateRepository: PostLastSeenDateRepository, ) : SwitchAccountUseCase { override suspend fun invoke(account: AccountModel) { val accountId = account.id ?: return @@ -39,6 +43,8 @@ internal class DefaultSwitchAccountUseCase( notificationCenter.send(NotificationCenterEvent.Logout) communitySortRepository.clear() + userSortRepository.clear() + postLastSeenDateRepository.clear() communityPreferredLanguageRepository.clear() serviceProvider.changeInstance(instance) diff --git a/unit/postdetail/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/unit/postdetail/PostDetailMviModel.kt b/unit/postdetail/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/unit/postdetail/PostDetailMviModel.kt index b6141f3fa..04cce87b5 100644 --- a/unit/postdetail/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/unit/postdetail/PostDetailMviModel.kt +++ b/unit/postdetail/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/unit/postdetail/PostDetailMviModel.kt @@ -150,6 +150,7 @@ interface PostDetailMviModel : val meTagColor: Int? = null, val modTagColor: Int? = null, val opTagColor: Int? = null, + val lastSeenTimestamp: Long? = null, ) sealed interface Effect { diff --git a/unit/postdetail/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/unit/postdetail/PostDetailScreen.kt b/unit/postdetail/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/unit/postdetail/PostDetailScreen.kt index 4a62a4a09..73eb0127f 100644 --- a/unit/postdetail/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/unit/postdetail/PostDetailScreen.kt +++ b/unit/postdetail/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/unit/postdetail/PostDetailScreen.kt @@ -120,6 +120,7 @@ import com.livefast.eattrash.raccoonforlemmy.core.persistence.data.ActionOnSwipe import com.livefast.eattrash.raccoonforlemmy.core.persistence.di.getSettingsRepository import com.livefast.eattrash.raccoonforlemmy.core.utils.VoteAction import com.livefast.eattrash.raccoonforlemmy.core.utils.compose.onClick +import com.livefast.eattrash.raccoonforlemmy.core.utils.datetime.toTimestamp import com.livefast.eattrash.raccoonforlemmy.core.utils.toIcon import com.livefast.eattrash.raccoonforlemmy.core.utils.toLocalDp import com.livefast.eattrash.raccoonforlemmy.core.utils.toModifier @@ -1063,22 +1064,33 @@ class PostDetailScreen( emptyList() }, content = { + val commentTs = + with(comment) { + updateDate ?: publishDate + }?.toTimestamp() + val lastSeenTs = uiState.lastSeenTimestamp + val isAfterLastSeenTs = commentTs != null && + lastSeenTs != null && + commentTs > lastSeenTs + val backgroundModifier = + when { + commentIdToHighlight == comment.id || + (commentIdToHighlight == null && isAfterLastSeenTs) + -> + Modifier.background( + MaterialTheme.colorScheme + .surfaceColorAtElevation( + 5.dp, + ).copy(alpha = 0.75f), + ) + + else -> Modifier + } CommentCard( modifier = Modifier .background(MaterialTheme.colorScheme.background) - .then( - if (comment.id == commentIdToHighlight) { - Modifier.background( - MaterialTheme.colorScheme - .surfaceColorAtElevation( - 5.dp, - ).copy(alpha = 0.75f), - ) - } else { - Modifier - }, - ), + .then(backgroundModifier), comment = comment, isOp = comment.creator?.id == uiState.post.creator?.id, showBot = true, diff --git a/unit/postdetail/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/unit/postdetail/PostDetailViewModel.kt b/unit/postdetail/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/unit/postdetail/PostDetailViewModel.kt index 0684c72c9..e33032464 100644 --- a/unit/postdetail/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/unit/postdetail/PostDetailViewModel.kt +++ b/unit/postdetail/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/unit/postdetail/PostDetailViewModel.kt @@ -7,8 +7,10 @@ import com.livefast.eattrash.raccoonforlemmy.core.notifications.NotificationCent import com.livefast.eattrash.raccoonforlemmy.core.notifications.NotificationCenterEvent import com.livefast.eattrash.raccoonforlemmy.core.persistence.data.UserTagType import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.AccountRepository +import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.PostLastSeenDateRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.SettingsRepository import com.livefast.eattrash.raccoonforlemmy.core.persistence.repository.UserTagRepository +import com.livefast.eattrash.raccoonforlemmy.core.utils.datetime.epochMillis import com.livefast.eattrash.raccoonforlemmy.core.utils.share.ShareHelper import com.livefast.eattrash.raccoonforlemmy.core.utils.vibrate.HapticFeedback import com.livefast.eattrash.raccoonforlemmy.domain.identity.repository.ApiConfigurationRepository @@ -60,6 +62,7 @@ class PostDetailViewModel( private val settingsRepository: SettingsRepository, private val accountRepository: AccountRepository, private val userTagRepository: UserTagRepository, + private val postLastSeenDateRepository: PostLastSeenDateRepository, private val userTagHelper: UserTagHelper, private val shareHelper: ShareHelper, private val notificationCenter: NotificationCenter, @@ -95,14 +98,20 @@ class PostDetailViewModel( } if (uiState.value.post.id == 0L) { val post = itemCache.getPost(postId) ?: PostModel() + val lastSeenTimestamp = postLastSeenDateRepository.get(postId) updateState { it.copy( post = post, isModerator = isModerator, currentUserId = identityRepository.cachedUser?.id, canFetchMore = it.comments.size < post.comments, + lastSeenTimestamp = lastSeenTimestamp, ) } + if (identityRepository.isLogged.value == true) { + val now = epochMillis() + postLastSeenDateRepository.save(postId = postId, timestamp = now) + } } themeRepository.postLayout diff --git a/unit/postdetail/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/unit/postdetail/di/PostDetailModule.kt b/unit/postdetail/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/unit/postdetail/di/PostDetailModule.kt index 588ce18fa..c684d6337 100644 --- a/unit/postdetail/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/unit/postdetail/di/PostDetailModule.kt +++ b/unit/postdetail/src/commonMain/kotlin/com/livefast/eattrash/raccoonforlemmy/unit/postdetail/di/PostDetailModule.kt @@ -35,6 +35,7 @@ val postDetailModule = accountRepository = instance(), userTagRepository = instance(), userTagHelper = instance(), + postLastSeenDateRepository = instance(), shareHelper = instance(), notificationCenter = instance(), hapticFeedback = instance(),