Implement SearchScreen

- Empty search screen if the search result is empty
- Topics contents that reuses the TopicsTabContent for InterestsScreen
- Updates contents that reuses the newsFeed for ForYouScreen

TODO: Needs to add RecentSearch
Change-Id: I19179336a60b5165e8b59508fb03dd8f55a16f96
search_screen
Takeshi Hagikura 2 years ago
parent 2eddf0ba7e
commit 13e6cc4f2b

@ -24,9 +24,12 @@ import com.google.samples.apps.nowinandroid.feature.bookmarks.navigation.bookmar
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouNavigationRoute
import com.google.samples.apps.nowinandroid.feature.foryou.navigation.forYouScreen
import com.google.samples.apps.nowinandroid.feature.interests.navigation.interestsGraph
import com.google.samples.apps.nowinandroid.feature.interests.navigation.navigateToInterestsGraph
import com.google.samples.apps.nowinandroid.feature.search.navigation.searchScreen
import com.google.samples.apps.nowinandroid.feature.topic.navigation.navigateToTopic
import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen
import com.google.samples.apps.nowinandroid.navigation.TopLevelDestination.INTERESTS
import com.google.samples.apps.nowinandroid.ui.NiaAppState
/**
* Top-level navigation graph. Navigation is organized as explained at
@ -37,10 +40,11 @@ import com.google.samples.apps.nowinandroid.feature.topic.navigation.topicScreen
*/
@Composable
fun NiaNavHost(
navController: NavHostController,
appState: NiaAppState,
modifier: Modifier = Modifier,
startDestination: String = forYouNavigationRoute,
) {
val navController = appState.navController
NavHost(
navController = navController,
startDestination = startDestination,
@ -49,7 +53,11 @@ fun NiaNavHost(
// TODO: handle topic clicks from each top level destination
forYouScreen(onTopicClick = {})
bookmarksScreen(onTopicClick = {})
searchScreen(onBackClick = navController::popBackStack)
searchScreen(
onBackClick = navController::popBackStack,
onInterestsClick = { appState.navigateToTopLevelDestination(INTERESTS) },
onTopicClick = {}
)
interestsGraph(
onTopicClick = { topicId ->
navController.navigateToTopic(topicId)

@ -181,7 +181,7 @@ fun NiaApp(
)
}
NiaNavHost(appState.navController)
NiaNavHost(appState)
}
// TODO: We may want to add padding or spacer when the snackbar is shown so that

@ -25,6 +25,7 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Vid
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import com.google.samples.apps.nowinandroid.core.ui.PreviewParameterData.newsResources
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
@ -36,9 +37,12 @@ import kotlinx.datetime.toInstant
* provides list of [UserNewsResource] for Composable previews.
*/
class UserNewsResourcePreviewParameterProvider : PreviewParameterProvider<List<UserNewsResource>> {
override val values: Sequence<List<UserNewsResource>>
get() {
val userData: UserData = UserData(
override val values: Sequence<List<UserNewsResource>> = sequenceOf(newsResources)
}
object PreviewParameterData {
private val userData: UserData = UserData(
bookmarkedNewsResources = setOf("1", "3"),
followedTopics = emptySet(),
themeBrand = ThemeBrand.ANDROID,
@ -74,8 +78,7 @@ class UserNewsResourcePreviewParameterProvider : PreviewParameterProvider<List<U
),
)
return sequenceOf(
listOf(
val newsResources = listOf(
UserNewsResource(
newsResource = NewsResource(
id = "1",
@ -129,7 +132,5 @@ class UserNewsResourcePreviewParameterProvider : PreviewParameterProvider<List<U
),
userData = userData,
),
),
)
}
}

@ -35,6 +35,7 @@ fun TopicsTabContent(
onTopicClick: (String) -> Unit,
onFollowButtonClick: (String, Boolean) -> Unit,
modifier: Modifier = Modifier,
withBottomSpacer: Boolean = true
) {
LazyColumn(
modifier = modifier
@ -56,8 +57,10 @@ fun TopicsTabContent(
}
}
if (withBottomSpacer) {
item {
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
}
}
}
}

@ -26,3 +26,9 @@ android {
namespace = "com.google.samples.apps.nowinandroid.feature.search"
}
dependencies {
implementation(project(":feature:foryou"))
implementation(project(":feature:interests"))
implementation(libs.kotlinx.datetime)
}

@ -17,13 +17,24 @@
package com.google.samples.apps.nowinandroid.feature.search
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onParent
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig.DARK
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand.ANDROID
import com.google.samples.apps.nowinandroid.core.model.data.UserData
import com.google.samples.apps.nowinandroid.core.testing.data.followableTopicTestData
import com.google.samples.apps.nowinandroid.core.testing.data.newsResourcesTestData
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import com.google.samples.apps.nowinandroid.feature.interests.R as interestsR
/**
* UI test for checking the correct behaviour of the Search screen.
@ -33,12 +44,34 @@ class SearchScreenTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
private lateinit var clearSearchText: String
private lateinit var clearSearchContentDesc: String
private lateinit var followButtonContentDesc: String
private lateinit var unfollowButtonContentDesc: String
private lateinit var topicsString: String
private lateinit var updatesString: String
private lateinit var tryAnotherSearchString: String
private val userData: UserData = UserData(
bookmarkedNewsResources = setOf("1", "3"),
followedTopics = emptySet(),
themeBrand = ANDROID,
darkThemeConfig = DARK,
shouldHideOnboarding = true,
useDynamicColor = false,
)
@Before
fun setup() {
composeTestRule.activity.apply {
clearSearchText = getString(R.string.clear_search_text)
clearSearchContentDesc = getString(R.string.clear_search_text_content_desc)
followButtonContentDesc =
getString(interestsR.string.card_follow_button_content_desc)
unfollowButtonContentDesc =
getString(interestsR.string.card_unfollow_button_content_desc)
topicsString = getString(R.string.topics)
updatesString = getString(R.string.updates)
tryAnotherSearchString = getString(R.string.try_another_search) +
" " + getString(R.string.interests) + " " + getString(R.string.to_browse_topics)
}
}
@ -49,10 +82,72 @@ class SearchScreenTest {
}
composeTestRule
.onNodeWithContentDescription(clearSearchText)
.onNodeWithContentDescription(clearSearchContentDesc)
// The parent of the IconButton whose contentDescription matches the clearSearchText
// should be the TextField for search
.onParent()
.assertIsFocused()
}
@Test
fun emptySearchResult_emptyScreenIsDisplayed() {
composeTestRule.setContent {
SearchScreen(
uiState = SearchResultUiState.Success()
)
}
composeTestRule
.onNodeWithText(tryAnotherSearchString)
.assertIsDisplayed()
}
@Test
fun searchResultWithTopics_allTopicsAreVisible_followButtonsVisibleForTheNumOfFollowedTopics() {
composeTestRule.setContent {
SearchScreen(
uiState = SearchResultUiState.Success(topics = followableTopicTestData),
)
}
composeTestRule
.onNodeWithText(topicsString)
.assertIsDisplayed()
composeTestRule
.onNodeWithText(followableTopicTestData[0].topic.name)
.assertIsDisplayed()
composeTestRule
.onNodeWithText(followableTopicTestData[1].topic.name)
.assertIsDisplayed()
composeTestRule
.onNodeWithText(followableTopicTestData[2].topic.name)
.assertIsDisplayed()
composeTestRule
.onAllNodesWithContentDescription(followButtonContentDesc)
.assertCountEquals(2)
composeTestRule
.onAllNodesWithContentDescription(unfollowButtonContentDesc)
.assertCountEquals(1)
}
@Test
fun searchResultWithNewsResources_firstNewsResourcesIsVisible() {
composeTestRule.setContent {
SearchScreen(
uiState = SearchResultUiState.Success(newsResources = newsResourcesTestData.map {
UserNewsResource(
newsResource = it,
userData = userData)
}),
)
}
composeTestRule
.onNodeWithText(updatesString)
.assertIsDisplayed()
composeTestRule
.onNodeWithText(newsResourcesTestData[0].title)
.assertIsDisplayed()
}
}

@ -0,0 +1,31 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.feature.search
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
sealed interface SearchResultUiState {
object Loading : SearchResultUiState
data class Success(
val topics: List<FollowableTopic> = emptyList(),
val newsResources: List<UserNewsResource> = emptyList(),
) : SearchResultUiState {
fun isEmpty(): Boolean = topics.isEmpty() && newsResources.isEmpty()
}
}

@ -16,24 +16,34 @@
package com.google.samples.apps.nowinandroid.feature.search
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.foundation.lazy.grid.GridCells.Adaptive
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@ -41,27 +51,51 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
import com.google.samples.apps.nowinandroid.core.ui.DevicePreviews
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import com.google.samples.apps.nowinandroid.core.ui.R.string
import com.google.samples.apps.nowinandroid.core.ui.TrackScreenViewEvent
import com.google.samples.apps.nowinandroid.core.ui.TrackScrollJank
import com.google.samples.apps.nowinandroid.core.ui.newsFeed
import com.google.samples.apps.nowinandroid.feature.foryou.ForYouViewModel
import com.google.samples.apps.nowinandroid.feature.interests.InterestsViewModel
import com.google.samples.apps.nowinandroid.feature.interests.TopicsTabContent
import com.google.samples.apps.nowinandroid.feature.search.R as searchR
@Composable
internal fun SearchRoute(
modifier: Modifier = Modifier,
onBackClick: () -> Unit,
viewModel: SearchViewModel = hiltViewModel(),
onInterestsClick: () -> Unit,
onTopicClick: (String) -> Unit,
interestsViewModel: InterestsViewModel = hiltViewModel(),
searchViewModel: SearchViewModel = hiltViewModel(),
forYouViewModel: ForYouViewModel = hiltViewModel(),
) {
SearchScreen(
modifier = modifier,
onBackClick = onBackClick,
onSearchQueryChanged = viewModel::onSearchQueryChanged,
onFollowButtonClick = interestsViewModel::followTopic,
onInterestsClick = onInterestsClick,
onSearchQueryChanged = searchViewModel::onSearchQueryChanged,
onTopicClick = onTopicClick,
onNewsResourcesCheckedChanged = forYouViewModel::updateNewsResourceSaved
)
}
@ -69,27 +103,161 @@ internal fun SearchRoute(
internal fun SearchScreen(
modifier: Modifier = Modifier,
onBackClick: () -> Unit = {},
onFollowButtonClick: (String, Boolean) -> Unit = {_, _ -> },
onInterestsClick: () -> Unit = {},
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit = {_, _ -> },
onSearchQueryChanged: (String) -> Unit = {},
onTopicClick: (String) -> Unit = {},
uiState: SearchResultUiState = SearchResultUiState.Loading,
) {
val searchQuery = remember { mutableStateOf("") }
TrackScreenViewEvent(screenName = "Search")
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Column(modifier = modifier) {
Spacer(Modifier.windowInsetsTopHeight(WindowInsets.safeDrawing))
SearchToolbar(
onBackClick = onBackClick,
onSearchQueryChanged = onSearchQueryChanged,
searchQuery = searchQuery,
)
when (uiState) {
SearchResultUiState.Loading -> Unit
is SearchResultUiState.Success -> {
if (uiState.isEmpty()) {
EmptySearchResultBody(
onInterestsClick = onInterestsClick,
searchQuery = searchQuery,
)
} else {
SearchResultBody(
topics = uiState.topics,
onFollowButtonClick = onFollowButtonClick,
onNewsResourcesCheckedChanged = onNewsResourcesCheckedChanged,
onTopicClick = onTopicClick,
newsResources = uiState.newsResources,
)
}
}
}
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing))
}
}
@Composable
fun EmptySearchResultBody(
onInterestsClick: () -> Unit = {},
searchQuery: MutableState<String>,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
val queryValue = searchQuery.value
val message = stringResource(id = searchR.string.search_result_not_found, queryValue)
val start = message.indexOf(queryValue)
Text(
text = AnnotatedString(
text = message,
spanStyles = listOf(
AnnotatedString.Range(
SpanStyle(fontWeight = FontWeight.Bold),
start = start,
end = start + queryValue.length,
),
),
),
modifier = Modifier.padding(horizontal = 36.dp, vertical = 24.dp),
)
val interests = stringResource(id = searchR.string.interests)
val tryAnotherSearchString = buildAnnotatedString {
append(stringResource(id = searchR.string.try_another_search))
append(" ")
withStyle(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
fontWeight = FontWeight.Bold,
),
) {
pushStringAnnotation(tag = interests, annotation = interests)
append(interests)
}
append(" ")
append(stringResource(id = searchR.string.to_browse_topics))
}
ClickableText(
text = tryAnotherSearchString,
modifier = Modifier
.padding(start = 36.dp, end = 36.dp, bottom = 24.dp)
.clickable {},
) { offset ->
tryAnotherSearchString.getStringAnnotations(start = offset, end = offset)
.firstOrNull()
?.let {
onInterestsClick()
}
}
}
}
@Composable
private fun SearchResultBody(
topics: List<FollowableTopic>,
newsResources: List<UserNewsResource>,
onFollowButtonClick: (String, Boolean) -> Unit,
onNewsResourcesCheckedChanged: (String, Boolean) -> Unit = {_, _ -> },
onTopicClick: (String) -> Unit = {}
) {
if (topics.isNotEmpty()) {
Text(
text = buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(stringResource(id = searchR.string.topics))
}
},
modifier = Modifier.padding(16.dp),
)
TopicsTabContent(
topics = topics,
onTopicClick = onTopicClick,
onFollowButtonClick = onFollowButtonClick,
withBottomSpacer = false
)
}
if (newsResources.isNotEmpty()) {
Text(
text = buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(stringResource(id = searchR.string.updates))
}
},
modifier = Modifier.padding(16.dp),
)
val state = rememberLazyGridState()
TrackScrollJank(scrollableState = state, stateName = "search:newsResource")
LazyVerticalGrid(
columns = Adaptive(300.dp),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
modifier = Modifier
.fillMaxSize()
.testTag("search:newsResources"),
state = state,
) {
newsFeed(
feedState = NewsFeedUiState.Success(feed = newsResources),
onNewsResourcesCheckedChanged = onNewsResourcesCheckedChanged,
onTopicClick = onTopicClick
)
}
}
}
@Composable
private fun SearchToolbar(
modifier: Modifier = Modifier,
onBackClick: () -> Unit = {},
onSearchQueryChanged: (String) -> Unit = {},
searchQuery: MutableState<String> = mutableStateOf(""),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
@ -103,14 +271,19 @@ private fun SearchToolbar(
),
)
}
SearchTextField(onSearchQueryChanged = onSearchQueryChanged)
SearchTextField(
onSearchQueryChanged = onSearchQueryChanged,
searchQuery = searchQuery,
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SearchTextField(onSearchQueryChanged: (String) -> Unit) {
val textState = remember { mutableStateOf("") }
private fun SearchTextField(
onSearchQueryChanged: (String) -> Unit,
searchQuery: MutableState<String>,
) {
val focusRequester = remember { FocusRequester() }
TextField(
colors = TextFieldDefaults.textFieldColors(
@ -128,26 +301,26 @@ private fun SearchTextField(onSearchQueryChanged: (String) -> Unit) {
)
},
trailingIcon = {
IconButton(onClick = { textState.value = "" }) {
IconButton(onClick = { searchQuery.value = "" }) {
Icon(
imageVector = NiaIcons.Close,
contentDescription = stringResource(
id = searchR.string.clear_search_text,
id = searchR.string.clear_search_text_content_desc,
),
tint = MaterialTheme.colorScheme.onSurface,
)
}
},
onValueChange = {
textState.value = it
searchQuery.value = it
onSearchQueryChanged(it)
},
modifier = Modifier
.fillMaxWidth()
.padding(12.dp)
.padding(16.dp)
.focusRequester(focusRequester),
shape = RoundedCornerShape(32.dp),
value = textState.value,
value = searchQuery.value,
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
@ -162,10 +335,22 @@ private fun SearchToolbarPreview() {
}
}
@Preview
@Composable
private fun EmptySearchResultColumnPreview() {
NiaTheme {
val searchQuery = remember { mutableStateOf("C++") }
EmptySearchResultBody(searchQuery = searchQuery)
}
}
@DevicePreviews
@Composable
private fun SearchScreenPreview() {
private fun SearchScreenPreview(
@PreviewParameter(SearchResultUiStatePreviewParameterProvider::class)
searchResultUiState: SearchResultUiState,
) {
NiaTheme {
SearchScreen()
SearchScreen(uiState = searchResultUiState)
}
}

@ -0,0 +1,36 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.feature.search
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
import com.google.samples.apps.nowinandroid.core.ui.PreviewParameterData.newsResources
import com.google.samples.apps.nowinandroid.core.ui.PreviewParameterData.topics
/* ktlint-disable max-line-length */
/**
* This [PreviewParameterProvider](https://developer.android.com/reference/kotlin/androidx/compose/ui/tooling/preview/PreviewParameterProvider)
* provides list of [SearchResultUiState] for Composable previews.
*/
class SearchResultUiStatePreviewParameterProvider : PreviewParameterProvider<SearchResultUiState> {
override val values: Sequence<SearchResultUiState> = sequenceOf(SearchResultUiState.Success(
topics = topics.mapIndexed { i, topic ->
FollowableTopic(topic = topic, isFollowed = i % 2 == 0)
},
newsResources = newsResources,
))
}

@ -28,10 +28,18 @@ fun NavController.navigateToSearch(navOptions: NavOptions? = null) {
this.navigate(searchRoute, navOptions)
}
fun NavGraphBuilder.searchScreen(onBackClick: () -> Unit) {
fun NavGraphBuilder.searchScreen(
onBackClick: () -> Unit,
onInterestsClick: () -> Unit,
onTopicClick: (String) -> Unit = {}
) {
// TODO: Handle back stack for each top-level destination. At the moment each top-level
// destination may have own search screen's back stack.
composable(route = searchRoute) {
SearchRoute(onBackClick = onBackClick)
SearchRoute(
onBackClick = onBackClick,
onInterestsClick = onInterestsClick,
onTopicClick = onTopicClick
)
}
}

@ -16,5 +16,11 @@
-->
<resources>
<string name="search">Search</string>
<string name="clear_search_text">Clear search text</string>
<string name="clear_search_text_content_desc">Clear search text</string>
<string name="search_result_not_found">Sorry, there is no content found for your search \"%1$s\"</string>
<string name="try_another_search">Try another search or explorer </string>
<string name="interests">Interests</string>
<string name="to_browse_topics"> to browse topics</string>
<string name="topics">Topics</string>
<string name="updates">Updates</string>
</resources>
Loading…
Cancel
Save