diff --git a/app/src/main/java/com/rob729/newsfeed/database/NewsDatabase.kt b/app/src/main/java/com/rob729/newsfeed/database/NewsDatabase.kt index b04c9c1..ce51ef8 100644 --- a/app/src/main/java/com/rob729/newsfeed/database/NewsDatabase.kt +++ b/app/src/main/java/com/rob729/newsfeed/database/NewsDatabase.kt @@ -1,6 +1,7 @@ package com.rob729.newsfeed.database import android.content.Context +import androidx.room.AutoMigration import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase @@ -8,7 +9,7 @@ import androidx.room.TypeConverters import com.rob729.newsfeed.model.database.NewsSourceDbData @TypeConverters(DataConverter::class) -@Database(entities = [NewsSourceDbData::class], version = 1, exportSchema = false) +@Database(entities = [NewsSourceDbData::class], version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)]) abstract class NewsDatabase : RoomDatabase() { abstract fun newsDao(): NewsDao @@ -29,7 +30,7 @@ abstract class NewsDatabase : RoomDatabase() { context.applicationContext, NewsDatabase::class.java, "news_database" - ).build() + ).fallbackToDestructiveMigration().build() INSTANCE = instance return instance diff --git a/app/src/main/java/com/rob729/newsfeed/initalizers/KoinInitializer.kt b/app/src/main/java/com/rob729/newsfeed/initalizers/KoinInitializer.kt index 2d03f70..e458fec 100644 --- a/app/src/main/java/com/rob729/newsfeed/initalizers/KoinInitializer.kt +++ b/app/src/main/java/com/rob729/newsfeed/initalizers/KoinInitializer.kt @@ -1,6 +1,9 @@ package com.rob729.newsfeed.initalizers import android.content.Context +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.preferencesDataStore +import androidx.datastore.preferences.preferencesDataStoreFile import androidx.startup.Initializer import com.pluto.plugins.network.okhttp.addPlutoOkhttpInterceptor import com.rob729.newsfeed.database.NewsDBDataSource @@ -9,6 +12,7 @@ import com.rob729.newsfeed.network.NewsApi import com.rob729.newsfeed.network.NewsApiDataSource import com.rob729.newsfeed.network.NewsApiDataSourceImpl import com.rob729.newsfeed.utils.Constants +import com.rob729.newsfeed.utils.SearchHistoryHelper import com.rob729.newsfeed.vm.HomeViewModel import com.rob729.newsfeed.vm.NewsRepository import com.rob729.newsfeed.vm.SearchViewModel @@ -54,6 +58,12 @@ class KoinInitializer : Initializer { retrofitInstance.create(NewsApi::class.java) } }, + module { + single { + val dataStore = PreferenceDataStoreFactory.create(produceFile = { context.preferencesDataStoreFile(Constants.PREFS_NAME) }) + SearchHistoryHelper(dataStore) + } + }, module { singleOf(::NewsApiDataSourceImpl) { bind() } singleOf(::NewsDBDataSource) diff --git a/app/src/main/java/com/rob729/newsfeed/model/api/NetworkArticle.kt b/app/src/main/java/com/rob729/newsfeed/model/api/NetworkArticle.kt index ae596c2..81a9707 100644 --- a/app/src/main/java/com/rob729/newsfeed/model/api/NetworkArticle.kt +++ b/app/src/main/java/com/rob729/newsfeed/model/api/NetworkArticle.kt @@ -3,6 +3,7 @@ package com.rob729.newsfeed.model.api import androidx.annotation.Keep import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import java.io.Serializable @Keep @JsonClass(generateAdapter = true) @@ -11,5 +12,12 @@ data class NetworkArticle( @Json(name = "url") val url: String, @Json(name = "urlToImage") val imageUrl: String?, @Json(name = "description") val description: String?, - @Json(name = "publishedAt") val publishedAt: String -) \ No newline at end of file + @Json(name = "publishedAt") val publishedAt: String, + @Json(name = "source") val source: ArticleSource? = null +): Serializable + +@Keep +@JsonClass(generateAdapter = true) +data class ArticleSource( + @Json(name = "name") val name: String? = null +): Serializable diff --git a/app/src/main/java/com/rob729/newsfeed/model/database/ArticleDbData.kt b/app/src/main/java/com/rob729/newsfeed/model/database/ArticleDbData.kt index cd5e0a4..e4484d9 100644 --- a/app/src/main/java/com/rob729/newsfeed/model/database/ArticleDbData.kt +++ b/app/src/main/java/com/rob729/newsfeed/model/database/ArticleDbData.kt @@ -7,5 +7,6 @@ data class ArticleDbData( @ColumnInfo(name = "url") val url: String, @ColumnInfo(name = "urlToImage") val imageUrl: String?, @ColumnInfo(name = "description") val description: String?, - @ColumnInfo(name = "publishedAt") val publishedAt: String + @ColumnInfo(name = "publishedAt") val publishedAt: String, + @ColumnInfo(name = "source") val source: String? ) \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/model/mapper/ArticleMapper.kt b/app/src/main/java/com/rob729/newsfeed/model/mapper/ArticleMapper.kt index 6bf92a3..49ae914 100644 --- a/app/src/main/java/com/rob729/newsfeed/model/mapper/ArticleMapper.kt +++ b/app/src/main/java/com/rob729/newsfeed/model/mapper/ArticleMapper.kt @@ -10,7 +10,8 @@ fun mapNetworkArticleToArticleDbData(networkArticle: NetworkArticle): ArticleDbD networkArticle.url, networkArticle.imageUrl, networkArticle.description, - networkArticle.publishedAt + networkArticle.publishedAt, + networkArticle.source?.name ) } @@ -22,7 +23,8 @@ fun mapArticleDbDataToNewsArticleUiData(articleDbData: ArticleDbData): NewsArtic articleDbData.description, articleDbData.imageUrl, articleDbData.url, - articleDbData.publishedAt + articleDbData.publishedAt, + articleDbData.source.orEmpty() ) } @@ -34,6 +36,7 @@ fun mapNetworkArticleToNewsArticleUiData(networkArticle: NetworkArticle): NewsAr networkArticle.description, networkArticle.imageUrl, networkArticle.url, - networkArticle.publishedAt + networkArticle.publishedAt, + networkArticle.source?.name ?: "" ) } \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/model/state/search/SearchState.kt b/app/src/main/java/com/rob729/newsfeed/model/state/search/SearchState.kt index e1937ca..b5b5a86 100644 --- a/app/src/main/java/com/rob729/newsfeed/model/state/search/SearchState.kt +++ b/app/src/main/java/com/rob729/newsfeed/model/state/search/SearchState.kt @@ -4,5 +4,7 @@ import com.rob729.newsfeed.model.state.UiStatus data class SearchState( val uiStatus: UiStatus = UiStatus.EmptyScreen, + val editTextInput: String = "", val searchQuery: String = "", + val searchHistoryList: List = listOf() ) \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/model/ui/NewsArticleUiData.kt b/app/src/main/java/com/rob729/newsfeed/model/ui/NewsArticleUiData.kt index 4b94de9..c3ff196 100644 --- a/app/src/main/java/com/rob729/newsfeed/model/ui/NewsArticleUiData.kt +++ b/app/src/main/java/com/rob729/newsfeed/model/ui/NewsArticleUiData.kt @@ -5,5 +5,6 @@ data class NewsArticleUiData( val description: String, val imageUrl: String, val url: String, - val publishedAt: String + val publishedAt: String, + val source: String ) diff --git a/app/src/main/java/com/rob729/newsfeed/network/NewsApi.kt b/app/src/main/java/com/rob729/newsfeed/network/NewsApi.kt index 9178fb8..7638d81 100644 --- a/app/src/main/java/com/rob729/newsfeed/network/NewsApi.kt +++ b/app/src/main/java/com/rob729/newsfeed/network/NewsApi.kt @@ -21,6 +21,7 @@ interface NewsApi { @Query("apiKey") apiKey: String, @Query("from") startDate: String, @Query("sortBy") sortBy: String, - @Query("language") language: String + @Query("language") language: String, + @Query("pageSize") pageSize: Int, ): Response } diff --git a/app/src/main/java/com/rob729/newsfeed/network/NewsApiDataSourceImpl.kt b/app/src/main/java/com/rob729/newsfeed/network/NewsApiDataSourceImpl.kt index 983f6b3..7e07b10 100644 --- a/app/src/main/java/com/rob729/newsfeed/network/NewsApiDataSourceImpl.kt +++ b/app/src/main/java/com/rob729/newsfeed/network/NewsApiDataSourceImpl.kt @@ -36,7 +36,8 @@ class NewsApiDataSourceImpl( BuildConfig.NEWS_FEED_API_KEY, startDate, Constants.SORT_RESULT_FILTER_PUBLISHED_AT, - Constants.API_RESULT_LANGUAGE + Constants.API_RESULT_LANGUAGE, + Constants.SEARCH_RESPONSE_PAGE_SIZE ) ) } diff --git a/app/src/main/java/com/rob729/newsfeed/ui/NewsActivity.kt b/app/src/main/java/com/rob729/newsfeed/ui/NewsActivity.kt index fb1d0ae..5e5afac 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/NewsActivity.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/NewsActivity.kt @@ -1,6 +1,7 @@ package com.rob729.newsfeed.ui import android.Manifest +import android.content.Context import android.content.pm.PackageManager import android.net.Uri import android.os.Build @@ -18,6 +19,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.core.content.ContextCompat +import androidx.datastore.preferences.preferencesDataStore import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -28,6 +30,8 @@ import com.rob729.newsfeed.ui.theme.NewsFeedTheme import com.rob729.newsfeed.utils.Constants import com.rob729.newsfeed.utils.NotificationHelper + +private val Context.dataStore by preferencesDataStore(name = Constants.PREFS_NAME) @OptIn(ExperimentalComposeUiApi::class) class NewsActivity : ComponentActivity() { diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingShimmer.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingShimmer.kt index e2eed8c..87b0ce3 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingShimmer.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingShimmer.kt @@ -1,43 +1,9 @@ package com.rob729.newsfeed.ui.components -import androidx.compose.animation.core.FastOutLinearInEasing -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween import androidx.compose.runtime.Composable -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color @Composable -fun LoadingShimmer() { - - val gradient = listOf( - Color.LightGray.copy(alpha = 0.7f), //darker grey (60% opacity) - Color.LightGray.copy(alpha = 0.3f), //lighter grey (20% opacity) - Color.LightGray.copy(alpha = 0.7f) - ) - - val transition = rememberInfiniteTransition() // animate infinite times - - val translateAnimation = transition.animateFloat( //animate the transition - initialValue = 0f, - targetValue = 1000f, - animationSpec = infiniteRepeatable( - animation = tween( - durationMillis = 1000, // duration for the animation - easing = FastOutLinearInEasing - ) - ) - ) - val brush = Brush.linearGradient( - colors = gradient, - start = Offset(200f, 200f), - end = Offset( - x = translateAnimation.value, - y = translateAnimation.value - ) - ) - ShimmerListItem(brush = brush) +fun LoadingShimmer(brush: Brush) { + NewsFeedItemShimmer(brush = brush) } \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingView.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingView.kt index ecf1009..9160cd0 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingView.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/LoadingView.kt @@ -1,14 +1,66 @@ package com.rob729.newsfeed.ui.components +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import com.rob729.newsfeed.utils.ScreenType @Composable -fun LoadingView() { - LazyColumn { - repeat(4) { - item { - LoadingShimmer() +fun LoadingView(screenType: ScreenType = ScreenType.HOME) { + val gradient = listOf( + Color.LightGray.copy(alpha = 0.8f), //darker grey (60% opacity) + Color.LightGray.copy(alpha = 0.3f), //lighter grey (20% opacity) + Color.LightGray.copy(alpha = 0.8f) + ) + + val transition = rememberInfiniteTransition(label = "loading_shimmer_transition") // animate infinite times + + val translateAnimation by transition.animateFloat( //animate the transition + initialValue = 0f, + targetValue = 1000f, + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = 1000, // duration for the animation + easing = LinearEasing + ) + ), label = "loading_shimmer_translate_anim" + ) + + val brush = Brush.linearGradient( + colors = gradient, + start = Offset(translateAnimation, translateAnimation), + end = Offset( + x = translateAnimation + 200f, + y = translateAnimation + 200f + ) + ) + + LazyColumn(modifier = Modifier.fillMaxSize()) { + when(screenType) { + ScreenType.HOME -> { + repeat(4) { + item { + NewsFeedItemShimmer(brush) + } + } + } + ScreenType.SEARCH -> { + repeat(7) { + item { + SearchResultShimmer(brush) + } + } } } } diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/ShimmerListItem.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/NewsFeedItemShimmer.kt similarity index 98% rename from app/src/main/java/com/rob729/newsfeed/ui/components/ShimmerListItem.kt rename to app/src/main/java/com/rob729/newsfeed/ui/components/NewsFeedItemShimmer.kt index cb24024..759a024 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/components/ShimmerListItem.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/NewsFeedItemShimmer.kt @@ -17,7 +17,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.unit.dp @Composable -fun ShimmerListItem(brush: Brush) { +fun NewsFeedItemShimmer(brush: Brush) { Card( modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 8.dp), diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/NoInternetView.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/NoInternetView.kt index e310ddf..a2c3e72 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/components/NoInternetView.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/NoInternetView.kt @@ -19,6 +19,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.rob729.newsfeed.R @@ -54,7 +56,8 @@ fun NoInternetView(onTryAgainClicked: () -> Unit) { fontSize = 32.sp, color = Color.White, fontWeight = FontWeight.SemiBold, - fontFamily = lexendDecaFontFamily + fontFamily = lexendDecaFontFamily, + textAlign = TextAlign.Center ) Image( @@ -81,4 +84,12 @@ fun NoInternetView(onTryAgainClicked: () -> Unit) { ) } } +} + +@Preview +@Composable +fun NoInternetViewPreview() { + NoInternetView { + + } } \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/NoSearchResultsFound.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/NoSearchResultsFound.kt new file mode 100644 index 0000000..83363ca --- /dev/null +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/NoSearchResultsFound.kt @@ -0,0 +1,82 @@ +package com.rob729.newsfeed.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.rob729.newsfeed.R +import com.rob729.newsfeed.ui.theme.lexendDecaFontFamily + +@Composable +fun NoSearchResultsFound() { + + val context = LocalContext.current + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.drawable.no_result_found), + contentDescription = "no results found", + contentScale = ContentScale.Fit, + modifier = Modifier + .fillMaxWidth(0.5f) + .padding(top = 36.dp) + .align(Alignment.CenterHorizontally) + .clip(RoundedCornerShape(8.dp)) + ) + Spacer(modifier = Modifier.height(24.dp)) + Text( + modifier = Modifier + .wrapContentWidth() + .align(Alignment.CenterHorizontally), + text = context.getString(R.string.no_result_found_title), + fontSize = 24.sp, + color = Color.White, + fontWeight = FontWeight.SemiBold, + fontFamily = lexendDecaFontFamily + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + modifier = Modifier + .wrapContentWidth() + .align(Alignment.CenterHorizontally) + .padding(top = 4.dp), + text = context.getString(R.string.no_result_found_subtitle), + fontSize = 14.sp, + color = Color.White, + fontWeight = FontWeight.Normal, + fontFamily = lexendDecaFontFamily, + textAlign = TextAlign.Center + ) + } +} + + +@Preview +@Composable +fun NoSearchResultsFoundPreview() { + NoSearchResultsFound() +} \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/SearchBar.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchBar.kt new file mode 100644 index 0000000..64004e4 --- /dev/null +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchBar.kt @@ -0,0 +1,65 @@ +package com.rob729.newsfeed.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import com.rob729.newsfeed.ui.theme.lexendDecaFontFamily + +@Composable +fun SearchBar(searchQuery: String, updateSearchQuery: (String) -> Unit, clearEditText: () -> Unit, onLeadingIconClick: () -> Unit) { + TextField( + modifier = Modifier + .padding(12.dp) + .fillMaxWidth() + .testTag("search_input_text_field"), + value = searchQuery, + textStyle = TextStyle(fontFamily = lexendDecaFontFamily), + onValueChange = { + updateSearchQuery(it) + }, + leadingIcon = { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "back", + modifier = Modifier.clickable { + onLeadingIconClick() + }) + }, + trailingIcon = { + if (searchQuery.isNotBlank()) { + Icon( + imageVector = Icons.Default.Clear, + contentDescription = "clear", + modifier = Modifier.clickable { + clearEditText() + }) + } + }, + placeholder = { Text("search here", fontFamily = lexendDecaFontFamily) }, + singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + shape = RoundedCornerShape(12.dp), + colors = TextFieldDefaults.colors( + disabledTextColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/SearchHistoryKeywordPill.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchHistoryKeywordPill.kt new file mode 100644 index 0000000..d72bcc1 --- /dev/null +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchHistoryKeywordPill.kt @@ -0,0 +1,42 @@ +package com.rob729.newsfeed.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.rob729.newsfeed.ui.theme.lexendDecaFontFamily + +@Composable +fun SearchHistoryKeywordPill( + searchHistoryQuery: String, + onSearchHistoryPillClick: (String) -> Unit +) { + Box(modifier = Modifier.padding(end = 8.dp, bottom = 8.dp)) { + Surface( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .clickable { onSearchHistoryPillClick(searchHistoryQuery) }, elevation = 4.dp + ) { + Text( + text = searchHistoryQuery, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + modifier = Modifier + .padding(horizontal = 12.dp, vertical = 8.dp), + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + fontFamily = lexendDecaFontFamily + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/SearchHistoryList.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchHistoryList.kt new file mode 100644 index 0000000..f95fe78 --- /dev/null +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchHistoryList.kt @@ -0,0 +1,22 @@ +package com.rob729.newsfeed.ui.components + +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun SearchHistoryList( + searchHistoryList: List?, + onSearchHistoryPillClick: (String) -> Unit +) { + searchHistoryList?.let { + FlowRow(modifier = Modifier.fillMaxSize()) { + it.forEach { searchQuery -> + SearchHistoryKeywordPill(searchHistoryQuery = searchQuery, onSearchHistoryPillClick) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/SearchResultItem.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchResultItem.kt new file mode 100644 index 0000000..5102f74 --- /dev/null +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchResultItem.kt @@ -0,0 +1,194 @@ +package com.rob729.newsfeed.ui.components + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Schedule +import androidx.compose.material.icons.filled.Share +import androidx.compose.material.icons.filled.Web +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import coil.compose.AsyncImage +import coil.request.CachePolicy +import coil.request.ImageRequest +import com.rob729.newsfeed.model.ui.NewsArticleUiData +import com.rob729.newsfeed.ui.theme.lexendDecaFontFamily + +@Composable +fun SearchResultItem( + newsArticleUiData: NewsArticleUiData, + modifier: Modifier = Modifier, + onItemClick: () -> Unit +) { + + val context = LocalContext.current + + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + + val scale by animateFloatAsState( + targetValue = if (isPressed) 0.95f else 1.0f, + animationSpec = tween(durationMillis = 150), label = "" + ) + + Surface( + modifier = modifier + .padding(8.dp) + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = onItemClick + ) + .scale(scale), + elevation = 4.dp, + shape = RoundedCornerShape(12.dp) + ) { + Column(modifier = Modifier.padding(8.dp)) { + Row { + Text( + text = newsArticleUiData.title, + maxLines = 2, + fontWeight = FontWeight.SemiBold, + modifier = Modifier + .padding(end = 6.dp) + .fillMaxWidth(0.65f), + textAlign = TextAlign.Start, + overflow = TextOverflow.Ellipsis, + fontFamily = lexendDecaFontFamily + ) + + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(newsArticleUiData.imageUrl) + .crossfade(true) + .crossfade(200) + .networkCachePolicy(CachePolicy.ENABLED) + .memoryCachePolicy(CachePolicy.ENABLED) + .diskCachePolicy(CachePolicy.DISABLED) + .build(), + contentDescription = null, + alignment = Alignment.CenterEnd, + contentScale = ContentScale.Crop, + modifier = Modifier + .aspectRatio(1.75f) + .clip(RoundedCornerShape(8.dp)), + ) + } + Spacer(modifier = Modifier.height(12.dp)) + ConstraintLayout( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp) + ) { + val (sourceIcon, sourceText, publishedTimeIcon, publishedTimeText, shareIcon) = createRefs() + + Icon( + imageVector = Icons.Default.Web, contentDescription = "source", + Modifier + .size(14.dp) + .padding(end = 4.dp) + .constrainAs(sourceIcon) { + start.linkTo(parent.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) + Text( + text = newsArticleUiData.source, + fontWeight = FontWeight.Light, + fontSize = 12.sp, + textAlign = TextAlign.End, + modifier = Modifier + .padding(end = 12.dp) + .constrainAs(sourceText) { + start.linkTo(sourceIcon.end) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) + Icon( + imageVector = Icons.Default.Schedule, contentDescription = "time", + Modifier + .size(14.dp) + .constrainAs(publishedTimeIcon) { + end.linkTo(publishedTimeText.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) + Text( + text = getHowOldIsArticle(newsArticleUiData.publishedAt), + fontWeight = FontWeight.Light, + fontSize = 12.sp, + textAlign = TextAlign.End, + modifier = Modifier + .padding(end = 12.dp) + .constrainAs(publishedTimeText) { + end.linkTo(shareIcon.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) + Icon( + imageVector = Icons.Filled.Share, + contentDescription = "share", + modifier + .size(14.dp) + .clickable { shareArticle(context, newsArticleUiData.url) } + .constrainAs(shareIcon) { + end.linkTo(parent.end) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) + } + } + } +} + +@Preview +@Composable +fun PreviewSearchResultItem() { + SearchResultItem( + NewsArticleUiData( + "News title", + "News Description", + "", + "", + "6 Sept 2023", + "news source" + ) + ) { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/ui/components/SearchResultShimmer.kt b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchResultShimmer.kt new file mode 100644 index 0000000..024939d --- /dev/null +++ b/app/src/main/java/com/rob729/newsfeed/ui/components/SearchResultShimmer.kt @@ -0,0 +1,79 @@ +package com.rob729.newsfeed.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.unit.dp + +@Composable +fun SearchResultShimmer(brush: Brush) { + Surface( + Modifier.padding(start = 12.dp, end = 12.dp, top = 12.dp), + elevation = 4.dp, + shape = RoundedCornerShape(12.dp) + ) { + Column(modifier = Modifier.padding(8.dp)) { + Row { + Column { + Spacer( + modifier = Modifier + .fillMaxWidth(.65f) + .height(15.dp) + .clip(RoundedCornerShape(8.dp)) + .background(brush) + ) + Spacer(modifier = Modifier.height(4.dp)) + Spacer( + modifier = Modifier + .fillMaxWidth(.65f) + .height(15.dp) + .clip(RoundedCornerShape(10.dp)) + .background(brush) + ) + } + Spacer(modifier = Modifier.width(6.dp)) + Spacer( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .aspectRatio(1.75f) + .background(brush) + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Row { + Spacer( + modifier = Modifier + .height(10.dp) + .fillMaxWidth(0.3f) + .clip(RoundedCornerShape(8.dp)) + .background(brush) + ) + Spacer( + modifier = Modifier + .height(10.dp) + .weight(1f) + ) + Spacer( + modifier = Modifier + .height(10.dp) + .fillMaxWidth(0.2f) + .clip(RoundedCornerShape(8.dp)) + .background(brush) + ) + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/ui/screen/EmptySearchScreen.kt b/app/src/main/java/com/rob729/newsfeed/ui/screen/EmptySearchScreen.kt new file mode 100644 index 0000000..d185feb --- /dev/null +++ b/app/src/main/java/com/rob729/newsfeed/ui/screen/EmptySearchScreen.kt @@ -0,0 +1,63 @@ +package com.rob729.newsfeed.ui.screen + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.rob729.newsfeed.R +import com.rob729.newsfeed.ui.components.SearchHistoryList +import com.rob729.newsfeed.ui.theme.lexendDecaFontFamily + +@Composable +fun EmptySearchScreen(searchHistoryList: List?, onSearchHistoryItemClick: (String) -> Unit, onClearSearchHistoryClick: () -> Unit) { + Surface(modifier = Modifier + .fillMaxWidth() + .fillMaxHeight()) { + + if (searchHistoryList.isNullOrEmpty().not()) { + Column(modifier = Modifier.padding(start = 12.dp, top = 12.dp)) { + Row { + Text( + modifier = Modifier.align(Alignment.CenterVertically), + text = "Recent searches", + color = MaterialTheme.colors.onSurface, + fontSize = 20.sp, + fontWeight = FontWeight.ExtraBold, + fontFamily = lexendDecaFontFamily, + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(end = 12.dp) + .clickable { + onClearSearchHistoryClick() + }, + text = "clear", + color = MaterialTheme.colors.secondary, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + fontFamily = lexendDecaFontFamily + ) + } + Spacer(modifier = Modifier.height(16.dp)) + SearchHistoryList(searchHistoryList, onSearchHistoryItemClick) + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/ui/screen/HomeScreen.kt b/app/src/main/java/com/rob729/newsfeed/ui/screen/HomeScreen.kt index 28bf92d..679a3cb 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/screen/HomeScreen.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/screen/HomeScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -170,8 +171,8 @@ fun HomeScreen( @Composable private fun LazyListState.isScrollingUp(): Boolean { - var previousIndex by remember(this) { mutableStateOf(firstVisibleItemIndex) } - var previousScrollOffset by remember(this) { mutableStateOf(firstVisibleItemScrollOffset) } + var previousIndex by remember(this) { mutableIntStateOf(firstVisibleItemIndex) } + var previousScrollOffset by remember(this) { mutableIntStateOf(firstVisibleItemScrollOffset) } return remember(this) { derivedStateOf { if (previousIndex != firstVisibleItemIndex) { diff --git a/app/src/main/java/com/rob729/newsfeed/ui/screen/SearchScreen.kt b/app/src/main/java/com/rob729/newsfeed/ui/screen/SearchScreen.kt index 19045a6..bf34f59 100644 --- a/app/src/main/java/com/rob729/newsfeed/ui/screen/SearchScreen.kt +++ b/app/src/main/java/com/rob729/newsfeed/ui/screen/SearchScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Clear -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -39,14 +38,15 @@ import com.rob729.newsfeed.R import com.rob729.newsfeed.model.state.UiStatus import com.rob729.newsfeed.model.state.search.SearchSideEffects import com.rob729.newsfeed.ui.components.LoadingView -import com.rob729.newsfeed.ui.components.NewsFeedItem +import com.rob729.newsfeed.ui.components.NoSearchResultsFound +import com.rob729.newsfeed.ui.components.SearchResultItem import com.rob729.newsfeed.ui.theme.lexendDecaFontFamily +import com.rob729.newsfeed.utils.ScreenType import com.rob729.newsfeed.vm.SearchViewModel import org.koin.androidx.compose.koinViewModel import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect -@OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchScreen( navController: NavHostController, @@ -55,7 +55,6 @@ fun SearchScreen( ) { val searchState = viewModel.collectAsState().value - val listState = rememberLazyListState() var active by rememberSaveable { mutableStateOf(false) } @@ -63,7 +62,7 @@ fun SearchScreen( viewModel.collectSideEffect { when (it) { is SearchSideEffects.SearchQueryChanged -> { - + viewModel.addSearchQueryToHistoryList(it.query) } is SearchSideEffects.SearchResultClicked -> { @@ -76,6 +75,7 @@ fun SearchScreen( modifier = Modifier .fillMaxSize() .background(Color.Black) + .testTag("search_screen_box") ) { Column { @@ -130,20 +130,30 @@ fun SearchScreen( } UiStatus.Loading -> { - LoadingView() + LoadingView(ScreenType.SEARCH) } is UiStatus.Success -> { - LazyColumn(Modifier.testTag("search_result_news_list"), listState) { - items(searchState.uiStatus.news) { item -> - NewsFeedItem(newsArticleUiData = item) { - viewModel.newsFeedItemClicked(item) + if (searchState.uiStatus.news.isEmpty()) { + NoSearchResultsFound() + } else { + LazyColumn(Modifier.testTag("search_result_news_list"), listState) { + items(searchState.uiStatus.news) { item -> + SearchResultItem(newsArticleUiData = item) { + viewModel.newsFeedItemClicked(item) + } } } } } - else -> {} + is UiStatus.EmptyScreen -> { + EmptySearchScreen( + searchHistoryList = searchState.searchHistoryList, + viewModel::searchHistoryItemClicked, + viewModel::clearSearchHistory + ) + } } } } diff --git a/app/src/main/java/com/rob729/newsfeed/utils/Constants.kt b/app/src/main/java/com/rob729/newsfeed/utils/Constants.kt index e7da52f..2960990 100644 --- a/app/src/main/java/com/rob729/newsfeed/utils/Constants.kt +++ b/app/src/main/java/com/rob729/newsfeed/utils/Constants.kt @@ -42,5 +42,11 @@ object Constants { ) const val ERROR_MESSAGE_PREFIX = "Something went wrong" const val API_RESULT_LANGUAGE = "en" + const val SEARCH_RESPONSE_PAGE_SIZE = 15 const val SORT_RESULT_FILTER_PUBLISHED_AT = "publishedAt" + const val PREFS_NAME = "prefs_name" +} + +enum class ScreenType { + HOME, SEARCH } \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/utils/SearchHistoryHelper.kt b/app/src/main/java/com/rob729/newsfeed/utils/SearchHistoryHelper.kt new file mode 100644 index 0000000..31147c4 --- /dev/null +++ b/app/src/main/java/com/rob729/newsfeed/utils/SearchHistoryHelper.kt @@ -0,0 +1,47 @@ +package com.rob729.newsfeed.utils + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.core.stringSetPreferencesKey +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import java.io.IOException + + +class SearchHistoryHelper(private val dataStore: DataStore) { + + private val SEARCH_HISTORY_LIST = stringSetPreferencesKey("search_history_list") + + val searchHistoryFlow = dataStore.data + .catch { exception -> + if (exception is IOException) { + emit(emptyPreferences()) + } else { + throw exception + } + }.map { + it[SEARCH_HISTORY_LIST] + } + + suspend fun addSearchQueryToHistoryList(searchQuery: String) { + if (searchQuery.isEmpty()) { + return + } + dataStore.edit { pref -> + pref[SEARCH_HISTORY_LIST]?.filterNot { searchQuery.contains(it) }?.toMutableList()?.also { + it.add(searchQuery) + pref[SEARCH_HISTORY_LIST] = it.toSet() + } ?: run { + pref[SEARCH_HISTORY_LIST] = setOf(searchQuery) + } + } + } + + suspend fun clearSearchHistory() { + dataStore.edit { pref -> + pref[SEARCH_HISTORY_LIST] = setOf() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/rob729/newsfeed/vm/SearchViewModel.kt b/app/src/main/java/com/rob729/newsfeed/vm/SearchViewModel.kt index b18916e..57cc9f7 100644 --- a/app/src/main/java/com/rob729/newsfeed/vm/SearchViewModel.kt +++ b/app/src/main/java/com/rob729/newsfeed/vm/SearchViewModel.kt @@ -9,6 +9,8 @@ import com.rob729.newsfeed.model.state.UiStatus import com.rob729.newsfeed.model.state.search.SearchSideEffects import com.rob729.newsfeed.model.state.search.SearchState import com.rob729.newsfeed.model.ui.NewsArticleUiData +import com.rob729.newsfeed.utils.SearchHistoryHelper +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChangedBy @@ -21,8 +23,10 @@ import org.orbitmvi.orbit.syntax.simple.postSideEffect import org.orbitmvi.orbit.syntax.simple.reduce import org.orbitmvi.orbit.viewmodel.container +@OptIn(FlowPreview::class) class SearchViewModel( private val newsRepository: NewsRepository, + private val searchHistoryHelper: SearchHistoryHelper ) : ViewModel(), ContainerHost { override val container: Container = container( @@ -31,22 +35,30 @@ class SearchViewModel( init { viewModelScope.launch { - container.stateFlow.debounce(1000).distinctUntilChangedBy { it.searchQuery } - .collectLatest { - searchNewsResultsForQuery(it.searchQuery) + container.stateFlow.debounce(1000).distinctUntilChangedBy { it.editTextInput }.collectLatest { + searchNewsResultsForQuery(it.editTextInput) + } + } + + viewModelScope.launch { + searchHistoryHelper.searchHistoryFlow.collectLatest { searchHistorySet -> + intent { + reduce { state.copy(searchHistoryList = searchHistorySet?.toList().orEmpty()) } } + } } } fun updateSearchQuery(query: String) = intent { - reduce { state.copy(searchQuery = query) } + reduce { state.copy(editTextInput = query) } } private fun searchNewsResultsForQuery(query: String) = intent { reduce { - state.copy(searchQuery = query) + state.copy(searchQuery = query, editTextInput = query) } + postSideEffect(SearchSideEffects.SearchQueryChanged(query)) if (query.isBlank()) { reduce { state.copy(uiStatus = UiStatus.EmptyScreen) } } else { @@ -60,6 +72,22 @@ class SearchViewModel( postSideEffect(SearchSideEffects.SearchResultClicked(item.url)) } + fun searchHistoryItemClicked(searchHistoryItemText: String) = searchNewsResultsForQuery(searchHistoryItemText) + + fun addSearchQueryToHistoryList(query: String) { + viewModelScope.launch { + searchHistoryHelper.addSearchQueryToHistoryList(query.trim()) + } + } + + fun clearSearchHistory() { + viewModelScope.launch { + searchHistoryHelper.clearSearchHistory() + } + } + + fun clearEditTextInput() = searchNewsResultsForQuery("") + private suspend fun SimpleSyntax.updateStateFromNewsResource( newsResource: NewsResource ) { @@ -80,7 +108,7 @@ class SearchViewModel( (newsResource.data as? NetworkNews)?.let { reduce { state.copy( - uiStatus = UiStatus.Success(it.networkArticles.mapNotNull(::mapNetworkArticleToNewsArticleUiData)) + uiStatus = UiStatus.Success(it.networkArticles.distinctBy { it.imageUrl }.mapNotNull(::mapNetworkArticleToNewsArticleUiData)) ) } } diff --git a/app/src/main/res/drawable/no_internet.webp b/app/src/main/res/drawable/no_internet.webp index 0879480..949fd81 100644 Binary files a/app/src/main/res/drawable/no_internet.webp and b/app/src/main/res/drawable/no_internet.webp differ diff --git a/app/src/main/res/drawable/no_result_found.xml b/app/src/main/res/drawable/no_result_found.xml new file mode 100644 index 0000000..3fc906f --- /dev/null +++ b/app/src/main/res/drawable/no_result_found.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 60c0f7a..0f90c9c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,5 +4,7 @@ Click here to see the latest things happening around the world Something went wrong No internet connection + No results found + Try adjusting your search to find what you are looking for Try again