diff --git a/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewItem.kt index 28a3817a6d5..acc7bb48c49 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewItem.kt @@ -38,6 +38,7 @@ import com.wire.android.navigation.ExternalUriDirection import com.wire.android.navigation.WelcomeToNewAndroidAppDestination import com.wire.android.ui.common.RowItemTemplate import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.shimmerPlaceholder import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireTypography @@ -51,6 +52,7 @@ fun WhatsNewItem( text: String? = null, @DrawableRes trailingIcon: Int? = null, onRowPressed: Clickable = Clickable(false), + isLoading: Boolean = false, ) { RowItemTemplate( title = { @@ -59,7 +61,9 @@ fun WhatsNewItem( style = if (boldTitle) MaterialTheme.wireTypography.body02 else MaterialTheme.wireTypography.body01, color = MaterialTheme.wireColorScheme.onBackground, text = title, - modifier = Modifier.padding(start = dimensions().spacing8x) + modifier = Modifier + .padding(start = dimensions().spacing8x) + .shimmerPlaceholder(visible = isLoading) ) } }, @@ -69,7 +73,9 @@ fun WhatsNewItem( style = MaterialTheme.wireTypography.label04, color = MaterialTheme.wireColorScheme.secondaryText, text = text, - modifier = Modifier.padding(start = dimensions().spacing8x, top = dimensions().spacing8x) + modifier = Modifier + .padding(start = dimensions().spacing8x, top = dimensions().spacing8x) + .shimmerPlaceholder(visible = isLoading) ) } }, @@ -82,6 +88,7 @@ fun WhatsNewItem( modifier = Modifier .defaultMinSize(dimensions().wireIconButtonSize) .padding(end = dimensions().spacing8x) + .shimmerPlaceholder(visible = isLoading) ) } ?: Icons.Filled.ChevronRight }, @@ -129,7 +136,7 @@ sealed class WhatsNewItem( @PreviewMultipleThemes @Composable -fun previewFileRestrictionDialog() { +fun PreviewFileRestrictionDialog() { WireTheme { WhatsNewItem( title = "What's new item", @@ -138,3 +145,16 @@ fun previewFileRestrictionDialog() { ) } } + +@PreviewMultipleThemes +@Composable +fun PreviewFileRestrictionDialogLoading() { + WireTheme { + WhatsNewItem( + title = "What's new item", + text = "This is the text of the item", + trailingIcon = R.drawable.ic_arrow_right, + isLoading = true, + ) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewScreen.kt index d06c69036a5..e1ee4023c77 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewScreen.kt @@ -77,26 +77,42 @@ fun WhatsNewScreenContent( items = buildList { add(WhatsNewItem.WelcomeToNewAndroidApp) }, - onItemClicked = onItemClicked + onItemClicked = onItemClicked, + isLoading = false, ) folderWithElements( header = context.getString(R.string.whats_new_release_notes_group_title), items = buildList { - state.releaseNotesItems.forEach { - add( - WhatsNewItem.AndroidReleaseNotes( - id = it.id, - title = UIText.DynamicString(it.title), - boldTitle = true, - text = UIText.DynamicString(it.publishDate), - url = it.link, + if (state.isLoading) { + for (i in 0..3) { + add( + WhatsNewItem.AndroidReleaseNotes( + id = "placeholder_$i", + title = UIText.DynamicString("Android X.X.X"), // this text won't be displayed + boldTitle = true, + text = UIText.DynamicString("01 Jan 2024"), // this text won't be displayed + url = "", + ) ) - ) + } + } else { + state.releaseNotesItems.forEach { + add( + WhatsNewItem.AndroidReleaseNotes( + id = it.id, + title = UIText.DynamicString(it.title), + boldTitle = true, + text = UIText.DynamicString(it.publishDate), + url = it.link, + ) + ) + } } add(WhatsNewItem.AllAndroidReleaseNotes) }, - onItemClicked = onItemClicked + onItemClicked = onItemClicked, + isLoading = state.isLoading, ) } } @@ -104,7 +120,8 @@ fun WhatsNewScreenContent( private fun LazyListScope.folderWithElements( header: String? = null, items: List, - onItemClicked: (WhatsNewItem) -> Unit + onItemClicked: (WhatsNewItem) -> Unit, + isLoading: Boolean, ) { folderWithElements( header = header?.uppercase(), @@ -114,8 +131,9 @@ private fun LazyListScope.folderWithElements( title = item.title.asString(), boldTitle = item.boldTitle, text = item.text?.asString(), - onRowPressed = remember { Clickable(enabled = true) { onItemClicked(item) } }, + onRowPressed = remember { Clickable(enabled = !isLoading) { onItemClicked(item) } }, trailingIcon = R.drawable.ic_arrow_right, + isLoading = isLoading, ) } } @@ -123,5 +141,11 @@ private fun LazyListScope.folderWithElements( @Preview(showBackground = false) @Composable fun PreviewWhatsNewScreen() { - WhatsNewScreenContent(WhatsNewState()) {} + WhatsNewScreenContent(WhatsNewState(isLoading = false)) {} +} + +@Preview(showBackground = false) +@Composable +fun PreviewWhatsNewScreenLoading() { + WhatsNewScreenContent(WhatsNewState(isLoading = true)) {} } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewState.kt b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewState.kt index bccfd251f88..7bb5cc9e022 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewState.kt @@ -18,6 +18,7 @@ package com.wire.android.ui.home.whatsnew data class WhatsNewState( + val isLoading: Boolean = false, val releaseNotesItems: List = emptyList() ) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModel.kt index 091c91086aa..82a7b87ea33 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModel.kt @@ -17,41 +17,43 @@ */ package com.wire.android.ui.home.whatsnew -import android.icu.text.SimpleDateFormat +import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.prof18.rssparser.RssParser -import com.wire.android.BuildConfig -import com.wire.android.util.sha256 +import com.wire.android.R import com.wire.android.util.toMediumOnlyDateTime import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import java.text.SimpleDateFormat import java.util.Locale import javax.inject.Inject @HiltViewModel -class WhatsNewViewModel @Inject constructor() : ViewModel() { +class WhatsNewViewModel @Inject constructor(context: Context) : ViewModel() { private val rssParser = RssParser() private val publishDateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.ENGLISH) - var state by mutableStateOf(WhatsNewState()) + var state by mutableStateOf(WhatsNewState(isLoading = true)) private set init { viewModelScope.launch { - if (BuildConfig.URL_RSS_RELEASE_NOTES.isNotBlank()) { - rssParser.getRssChannel(BuildConfig.URL_RSS_RELEASE_NOTES).let { + val feedUrl = context.resources.getString(R.string.url_android_release_notes_feed) + if (feedUrl.isNotBlank()) { + rssParser.getRssChannel(feedUrl).let { state = state.copy( + isLoading = false, releaseNotesItems = it.items .map { item -> ReleaseNotesItem( - id = item.guid.orEmpty().sha256(), + id = item.guid.orEmpty(), title = item.title.orEmpty(), link = item.link.orEmpty(), - publishDate = item.pubDate?.let { publishDateFormat.parse(it).toMediumOnlyDateTime() }.orEmpty(), + publishDate = item.pubDate?.let { publishDateFormat.parse(it)?.toMediumOnlyDateTime() }.orEmpty(), ) } .filter { @@ -59,6 +61,11 @@ class WhatsNewViewModel @Inject constructor() : ViewModel() { } ) } + } else { + state = state.copy( + isLoading = false, + releaseNotesItems = emptyList() + ) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b0752cab08c..710f56671a3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -184,6 +184,7 @@ https://support.wire.com/hc/categories/4719917054365-Federation https://support.wire.com/hc/articles/115004082129 https://medium.com/wire-news/android-updates/home + https://medium.com/feed/wire-news/tagged/android http://maps.google.com/maps?z=%1d&q=loc:%2f+%2f Vault diff --git a/app/src/test/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModelTest.kt new file mode 100644 index 00000000000..ececc22a931 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/home/whatsnew/WhatsNewViewModelTest.kt @@ -0,0 +1,143 @@ +/* + * Wire + * Copyright (C) 2024 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.whatsnew + +import android.content.Context +import com.prof18.rssparser.RssParser +import com.prof18.rssparser.model.RssChannel +import com.prof18.rssparser.model.RssItem +import com.wire.android.R +import com.wire.android.config.CoroutineTestExtension +import com.wire.android.util.toMediumOnlyDateTime +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.impl.annotations.MockK +import io.mockk.mockkStatic +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.internal.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.text.SimpleDateFormat +import java.util.Date + +@ExtendWith(CoroutineTestExtension::class) +class WhatsNewViewModelTest { + + @Test + fun `given url is not blank, when fetching release notes, then execute getRssChannel`() = runTest { + val url = "url" + val (arrangement, viewModel) = Arrangement() + .withFeedResult(testRssChannel) + .withFeedUrl(url) + .arrange() + + advanceUntilIdle() + + assertEquals(testReleaseNotes, viewModel.state.releaseNotesItems) + assertEquals(false, viewModel.state.isLoading) + coVerify(exactly = 1) { + arrangement.rssParser.getRssChannel(url) + } + } + + @Test + fun `given url is blank, when fetching release notes, then do not execute getRssChannel`() = runTest { + val url = "" + val (arrangement, viewModel) = Arrangement() + .withFeedUrl(url) + .arrange() + + advanceUntilIdle() + + assertEquals(emptyList(), viewModel.state.releaseNotesItems) + assertEquals(false, viewModel.state.isLoading) + coVerify(exactly = 0) { + arrangement.rssParser.getRssChannel(url) + } + } + + inner class Arrangement { + + @MockK + lateinit var context: Context + + @MockK + lateinit var rssParser: RssParser + + val viewModel by lazy { + WhatsNewViewModel(context) + } + + fun withFeedUrl(feedUrl: String) = apply { + coEvery { context.resources.getString(R.string.url_android_release_notes_feed) } returns feedUrl + } + + fun withFeedResult(rssChannel: RssChannel) = apply { + coEvery { rssParser.getRssChannel(any()) } returns rssChannel + } + + init { + MockKAnnotations.init(this, relaxUnitFun = true) + mockkStatic(::RssParser) + coEvery { RssParser() } returns rssParser + mockkStatic(Date::toMediumOnlyDateTime) + coEvery { any().toMediumOnlyDateTime() } answers { + SimpleDateFormat("dd MMM yyyy").format(firstArg()) + } + } + + fun arrange() = this to viewModel + } + + private val testRssItem = RssItem( + guid = "guid", + title = "itemTitle", + author = "author", + link = "link", + pubDate = "Mon, 01 Jan 2024 00:00:00", + description = null, + content = null, + image = null, + audio = null, + video = null, + sourceName = null, + sourceUrl = null, + categories = emptyList(), + itunesItemData = null, + commentsUrl = null, + ) + private val testRssChannel: RssChannel = RssChannel( + title = "title", + items = listOf(testRssItem), + link = null, + description = null, + image = null, + lastBuildDate = null, + updatePeriod = null, + itunesChannelData = null, + ) + private val testReleaseNoteItem = ReleaseNotesItem( + id = "guid", + title = "itemTitle", + link = "link", + publishDate = "01 Jan 2024", + ) + private val testReleaseNotes: List = listOf(testReleaseNoteItem) +} diff --git a/default.json b/default.json index 47e1a9c53ab..3dbbb90c5a0 100644 --- a/default.json +++ b/default.json @@ -119,7 +119,6 @@ ] }, "is_password_protected_guest_link_enabled": false, - "url_rss_release_notes": "https://medium.com/feed/wire-news/tagged/android", "team_app_lock": false, "team_app_lock_timeout": 60, "max_remote_search_result_count": 30,