diff --git a/android/app/src/main/java/com/emergetools/hackernews/data/ChromeTabs.kt b/android/app/src/main/java/com/emergetools/hackernews/ChromeTabs.kt similarity index 93% rename from android/app/src/main/java/com/emergetools/hackernews/data/ChromeTabs.kt rename to android/app/src/main/java/com/emergetools/hackernews/ChromeTabs.kt index 527f4355..a2f264cb 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/data/ChromeTabs.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/ChromeTabs.kt @@ -1,4 +1,4 @@ -package com.emergetools.hackernews.data +package com.emergetools.hackernews import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.runtime.Composable diff --git a/android/app/src/main/java/com/emergetools/hackernews/HackerNewsApplication.kt b/android/app/src/main/java/com/emergetools/hackernews/HackerNewsApplication.kt index 0b3de615..e30d1040 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/HackerNewsApplication.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/HackerNewsApplication.kt @@ -6,29 +6,27 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore import androidx.room.Room -import com.emergetools.hackernews.data.BookmarkDao -import com.emergetools.hackernews.data.HackerNewsBaseDataSource -import com.emergetools.hackernews.data.HackerNewsDatabase -import com.emergetools.hackernews.data.HackerNewsSearchClient -import com.emergetools.hackernews.data.HackerNewsWebClient -import com.emergetools.hackernews.data.ItemRepository -import com.emergetools.hackernews.data.LocalCookieJar -import com.emergetools.hackernews.data.UserStorage +import com.emergetools.hackernews.data.local.BookmarkDao +import com.emergetools.hackernews.data.local.HackerNewsDatabase +import com.emergetools.hackernews.data.local.LocalCookieJar +import com.emergetools.hackernews.data.local.UserStorage +import com.emergetools.hackernews.data.remote.HackerNewsBaseClient +import com.emergetools.hackernews.data.remote.HackerNewsSearchClient +import com.emergetools.hackernews.data.remote.HackerNewsWebClient import kotlinx.serialization.json.Json import okhttp3.OkHttpClient import java.time.Duration -class HackerNewsApplication: Application() { +class HackerNewsApplication : Application() { private val json = Json { ignoreUnknownKeys = true } private lateinit var httpClient: OkHttpClient - private lateinit var baseClient: HackerNewsBaseDataSource lateinit var bookmarkDao: BookmarkDao lateinit var userStorage: UserStorage lateinit var searchClient: HackerNewsSearchClient lateinit var webClient: HackerNewsWebClient - lateinit var itemRepository: ItemRepository + lateinit var baseClient: HackerNewsBaseClient override fun onCreate() { super.onCreate() @@ -47,17 +45,16 @@ class HackerNewsApplication: Application() { .cookieJar(LocalCookieJar(userStorage)) .build() - baseClient = HackerNewsBaseDataSource(json, httpClient) searchClient = HackerNewsSearchClient(json, httpClient) webClient = HackerNewsWebClient(httpClient) - itemRepository = ItemRepository(baseClient) + baseClient = HackerNewsBaseClient(json, httpClient) } } val Context.dataStore: DataStore by preferencesDataStore(name = "user") -fun Context.itemRepository(): ItemRepository { - return (this.applicationContext as HackerNewsApplication).itemRepository +fun Context.baseClient(): HackerNewsBaseClient { + return (this.applicationContext as HackerNewsApplication).baseClient } fun Context.userStorage(): UserStorage { diff --git a/android/app/src/main/java/com/emergetools/hackernews/features/comments/HtmlToAnnotatedString.kt b/android/app/src/main/java/com/emergetools/hackernews/HtmlToAnnotatedString.kt similarity index 98% rename from android/app/src/main/java/com/emergetools/hackernews/features/comments/HtmlToAnnotatedString.kt rename to android/app/src/main/java/com/emergetools/hackernews/HtmlToAnnotatedString.kt index 8aa384ad..d992cd43 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/features/comments/HtmlToAnnotatedString.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/HtmlToAnnotatedString.kt @@ -1,4 +1,4 @@ -package com.emergetools.hackernews.features.comments +package com.emergetools.hackernews import android.graphics.Typeface import android.text.Layout @@ -151,4 +151,4 @@ private fun StyleSpan.toSpanStyle(): SpanStyle? { private fun TypefaceSpan.toSpanStyle(): SpanStyle { val fontFamily = this.typeface?.let { FontFamily(it) } return SpanStyle(fontFamily = fontFamily) -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/emergetools/hackernews/MainActivity.kt b/android/app/src/main/java/com/emergetools/hackernews/MainActivity.kt index ad9d1b20..07d54cab 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/MainActivity.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/MainActivity.kt @@ -27,7 +27,6 @@ import androidx.navigation.NavHostController import androidx.navigation.Navigator import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController -import com.emergetools.hackernews.data.ChromeTabsProvider import com.emergetools.hackernews.features.bookmarks.bookmarksRoutes import com.emergetools.hackernews.features.comments.commentsRoutes import com.emergetools.hackernews.features.login.loginRoutes diff --git a/android/app/src/main/java/com/emergetools/hackernews/data/HackerNewsBaseDataSource.kt b/android/app/src/main/java/com/emergetools/hackernews/data/HackerNewsBaseDataSource.kt deleted file mode 100644 index 2e60e5fb..00000000 --- a/android/app/src/main/java/com/emergetools/hackernews/data/HackerNewsBaseDataSource.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.emergetools.hackernews.data - -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonContentPolymorphicSerializer -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonNull -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import retrofit2.Retrofit -import retrofit2.converter.kotlinx.serialization.asConverterFactory -import retrofit2.http.GET -import retrofit2.http.Path - -const val BASE_FIREBASE_URL = "https://hacker-news.firebaseio.com/v0/" - -@Serializable(with = BaseResponseSerializer::class) -sealed interface BaseResponse { - @Serializable - data class Item( - val id: Long, - val type: String, - val time: Long, - val by: String? = null, - val title: String? = null, - val score: Int? = null, - val url: String? = null, - val descendants: Int? = null, - val kids: List? = null, - val text: String? = null - ): BaseResponse - - @Serializable - data object NullResponse: BaseResponse -} - -object BaseResponseSerializer : JsonContentPolymorphicSerializer(BaseResponse::class) { - override fun selectDeserializer(element: JsonElement) = when { - element is JsonNull -> BaseResponse.NullResponse.serializer() - else -> BaseResponse.Item.serializer() - } -} - -interface HackerNewsBaseApi { - @GET("topstories.json") - suspend fun getTopStoryIds(): List - - @GET("newstories.json") - suspend fun getNewStoryIds(): List - - @GET("item/{id}.json") - suspend fun getItem(@Path("id") itemId: Long): BaseResponse -} - -class HackerNewsBaseDataSource(json: Json, client: OkHttpClient) { - private val retrofit = Retrofit.Builder() - .baseUrl(BASE_FIREBASE_URL) - .addConverterFactory(json.asConverterFactory("application/json; charset=UTF8".toMediaType())) - .client(client) - .build() - - val api = retrofit.create(HackerNewsBaseApi::class.java) -} \ No newline at end of file diff --git a/android/app/src/main/java/com/emergetools/hackernews/data/ItemRepository.kt b/android/app/src/main/java/com/emergetools/hackernews/data/ItemRepository.kt deleted file mode 100644 index e488f045..00000000 --- a/android/app/src/main/java/com/emergetools/hackernews/data/ItemRepository.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.emergetools.hackernews.data - -import android.util.Log -import com.emergetools.hackernews.data.BaseResponse.Item -import com.emergetools.hackernews.features.stories.FeedType -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -typealias ItemId = Long -typealias Page = List - -sealed class FeedIdResponse(val page: Page) { - class Success(page: Page) : FeedIdResponse(page) - data class Error(val message: String) : FeedIdResponse(emptyList()) -} - -class ItemRepository( - private val baseClient: HackerNewsBaseDataSource, -) { - suspend fun getFeedIds(type: FeedType): FeedIdResponse { - return withContext(Dispatchers.IO) { - when (type) { - FeedType.Top -> { - try { - val result = baseClient.api.getTopStoryIds() - FeedIdResponse.Success(result) - } catch (error: Exception) { - FeedIdResponse.Error(error.message.orEmpty()) - } - } - - FeedType.New -> { - try { - val result = baseClient.api.getNewStoryIds() - FeedIdResponse.Success(result) - } catch (error: Exception) { - FeedIdResponse.Error(error.message.orEmpty()) - } - } - } - } - } - - suspend fun getPage(page: Page): List { - Log.d("Feed", "Loading Page: $page") - return withContext(Dispatchers.IO) { - val result = mutableListOf() - page.forEach { itemId -> - try { - val response = baseClient.api.getItem(itemId) - if (response is Item) { - result.add(response) - } - } catch (_: Exception) { - } - } - result.toList() - } - } -} - -fun MutableList.next() = removeAt(0) - - diff --git a/android/app/src/main/java/com/emergetools/hackernews/data/LocalDataSource.kt b/android/app/src/main/java/com/emergetools/hackernews/data/local/BookmarkStore.kt similarity index 90% rename from android/app/src/main/java/com/emergetools/hackernews/data/LocalDataSource.kt rename to android/app/src/main/java/com/emergetools/hackernews/data/local/BookmarkStore.kt index a62f4350..193997f7 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/data/LocalDataSource.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/data/local/BookmarkStore.kt @@ -1,4 +1,4 @@ -package com.emergetools.hackernews.data +package com.emergetools.hackernews.data.local import androidx.room.ColumnInfo import androidx.room.Dao @@ -36,7 +36,7 @@ interface BookmarkDao { } @Database(entities = [LocalBookmark::class], version = 1) -abstract class HackerNewsDatabase: RoomDatabase() { +abstract class HackerNewsDatabase : RoomDatabase() { abstract fun bookmarkDao(): BookmarkDao } diff --git a/android/app/src/main/java/com/emergetools/hackernews/data/LocalCookieJar.kt b/android/app/src/main/java/com/emergetools/hackernews/data/local/LocalCookieJar.kt similarity index 94% rename from android/app/src/main/java/com/emergetools/hackernews/data/LocalCookieJar.kt rename to android/app/src/main/java/com/emergetools/hackernews/data/local/LocalCookieJar.kt index 65854d20..f753db56 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/data/LocalCookieJar.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/data/local/LocalCookieJar.kt @@ -1,4 +1,4 @@ -package com.emergetools.hackernews.data +package com.emergetools.hackernews.data.local import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking @@ -27,4 +27,4 @@ class LocalCookieJar(private val userStorage: UserStorage) : CookieJar { emptyList() } } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/emergetools/hackernews/data/UserStorage.kt b/android/app/src/main/java/com/emergetools/hackernews/data/local/UserStorage.kt similarity index 93% rename from android/app/src/main/java/com/emergetools/hackernews/data/UserStorage.kt rename to android/app/src/main/java/com/emergetools/hackernews/data/local/UserStorage.kt index 6bfd87dc..260feb61 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/data/UserStorage.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/data/local/UserStorage.kt @@ -1,4 +1,4 @@ -package com.emergetools.hackernews.data +package com.emergetools.hackernews.data.local import android.content.Context import androidx.datastore.preferences.core.edit @@ -25,4 +25,4 @@ class UserStorage(private val appContext: Context) { fun getCookie(): Flow { return appContext.dataStore.data.map { it[cookieKey] } } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/emergetools/hackernews/data/remote/HackerNewsBaseApi.kt b/android/app/src/main/java/com/emergetools/hackernews/data/remote/HackerNewsBaseApi.kt new file mode 100644 index 00000000..0d3ef19d --- /dev/null +++ b/android/app/src/main/java/com/emergetools/hackernews/data/remote/HackerNewsBaseApi.kt @@ -0,0 +1,15 @@ +package com.emergetools.hackernews.data.remote + +import retrofit2.http.GET +import retrofit2.http.Path + +interface HackerNewsBaseApi { + @GET("topstories.json") + suspend fun getTopStoryIds(): List + + @GET("newstories.json") + suspend fun getNewStoryIds(): List + + @GET("item/{id}.json") + suspend fun getItem(@Path("id") itemId: Long): ItemResponse +} diff --git a/android/app/src/main/java/com/emergetools/hackernews/data/remote/HackerNewsBaseClient.kt b/android/app/src/main/java/com/emergetools/hackernews/data/remote/HackerNewsBaseClient.kt new file mode 100644 index 00000000..9c8c8e60 --- /dev/null +++ b/android/app/src/main/java/com/emergetools/hackernews/data/remote/HackerNewsBaseClient.kt @@ -0,0 +1,118 @@ +package com.emergetools.hackernews.data.remote + +import android.util.Log +import com.emergetools.hackernews.data.remote.ItemResponse.Item +import com.emergetools.hackernews.features.stories.FeedType +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonContentPolymorphicSerializer +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.kotlinx.serialization.asConverterFactory + +typealias ItemId = Long +typealias Page = List + +private const val BASE_FIREBASE_URL = "https://hacker-news.firebaseio.com/v0/" + +@Serializable(with = ItemResponseSerializer::class) +sealed interface ItemResponse { + @Serializable + data class Item( + val id: Long, + val type: String, + val time: Long, + val by: String? = null, + val title: String? = null, + val score: Int? = null, + val url: String? = null, + val descendants: Int? = null, + val kids: List? = null, + val text: String? = null + ) : ItemResponse + + @Serializable + data object NullResponse : ItemResponse +} + +private object ItemResponseSerializer : + JsonContentPolymorphicSerializer(ItemResponse::class) { + override fun selectDeserializer(element: JsonElement) = when { + element is JsonNull -> ItemResponse.NullResponse.serializer() + else -> Item.serializer() + } +} + +sealed class FeedIdResponse(val page: Page) { + class Success(page: Page) : FeedIdResponse(page) + data class Error(val message: String) : FeedIdResponse(emptyList()) +} + +class HackerNewsBaseClient( + json: Json, + client: OkHttpClient, +) { + private val retrofit = Retrofit.Builder() + .baseUrl(BASE_FIREBASE_URL) + .addConverterFactory(json.asConverterFactory("application/json; charset=UTF8".toMediaType())) + .client(client) + .build() + + private val api = retrofit.create(HackerNewsBaseApi::class.java) + + suspend fun getFeedIds(type: FeedType): FeedIdResponse { + return withContext(Dispatchers.IO) { + when (type) { + FeedType.Top -> { + try { + val result = api.getTopStoryIds() + FeedIdResponse.Success(result) + } catch (error: Exception) { + FeedIdResponse.Error(error.message.orEmpty()) + } + } + + FeedType.New -> { + try { + val result = api.getNewStoryIds() + FeedIdResponse.Success(result) + } catch (error: Exception) { + FeedIdResponse.Error(error.message.orEmpty()) + } + } + } + } + } + + suspend fun getPage(page: Page): List { + Log.d("Feed", "Loading Page: $page") + return withContext(Dispatchers.IO) { + val result = mutableListOf() + page.forEach { itemId -> + try { + val response = api.getItem(itemId) + if (response is Item) { + result.add(response) + } + } catch (_: Exception) { + } + } + result.toList() + } + } + + suspend fun getItem(itemId: Long): ItemResponse { + return withContext(Dispatchers.IO) { + api.getItem(itemId) + } + } +} + +fun MutableList.next() = removeAt(0) + + diff --git a/android/app/src/main/java/com/emergetools/hackernews/data/HackerNewsSearchClient.kt b/android/app/src/main/java/com/emergetools/hackernews/data/remote/HackerNewsSearchClient.kt similarity index 97% rename from android/app/src/main/java/com/emergetools/hackernews/data/HackerNewsSearchClient.kt rename to android/app/src/main/java/com/emergetools/hackernews/data/remote/HackerNewsSearchClient.kt index d9f450e4..43a5bd8e 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/data/HackerNewsSearchClient.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/data/remote/HackerNewsSearchClient.kt @@ -1,4 +1,4 @@ -package com.emergetools.hackernews.data +package com.emergetools.hackernews.data.remote import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/android/app/src/main/java/com/emergetools/hackernews/data/HackerNewsWebClient.kt b/android/app/src/main/java/com/emergetools/hackernews/data/remote/HackerNewsWebClient.kt similarity index 99% rename from android/app/src/main/java/com/emergetools/hackernews/data/HackerNewsWebClient.kt rename to android/app/src/main/java/com/emergetools/hackernews/data/remote/HackerNewsWebClient.kt index 8fbe5045..0c7d33d4 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/data/HackerNewsWebClient.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/data/remote/HackerNewsWebClient.kt @@ -1,4 +1,4 @@ -package com.emergetools.hackernews.data +package com.emergetools.hackernews.data.remote import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/android/app/src/main/java/com/emergetools/hackernews/features/bookmarks/BookmarksDomain.kt b/android/app/src/main/java/com/emergetools/hackernews/features/bookmarks/BookmarksDomain.kt index 6ce36f64..f709252a 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/features/bookmarks/BookmarksDomain.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/features/bookmarks/BookmarksDomain.kt @@ -3,8 +3,8 @@ package com.emergetools.hackernews.features.bookmarks import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.emergetools.hackernews.data.BookmarkDao -import com.emergetools.hackernews.data.LocalBookmark +import com.emergetools.hackernews.data.local.BookmarkDao +import com.emergetools.hackernews.data.local.LocalBookmark import com.emergetools.hackernews.data.relativeTimeStamp import com.emergetools.hackernews.features.comments.CommentsDestinations import com.emergetools.hackernews.features.stories.StoriesDestinations @@ -20,7 +20,7 @@ data class BookmarksState( ) sealed interface BookmarksAction { - data class RemoveBookmark(val storyItem: StoryItem.Content): BookmarksAction + data class RemoveBookmark(val storyItem: StoryItem.Content) : BookmarksAction } sealed interface BookmarksNavigation { @@ -87,4 +87,4 @@ fun LocalBookmark.toStoryItem(): StoryItem.Content { epochTimestamp = this.timestamp, timeLabel = relativeTimeStamp(this.timestamp) ) -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/emergetools/hackernews/features/bookmarks/BookmarksRouting.kt b/android/app/src/main/java/com/emergetools/hackernews/features/bookmarks/BookmarksRouting.kt index 968813c6..5314f41d 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/features/bookmarks/BookmarksRouting.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/features/bookmarks/BookmarksRouting.kt @@ -1,17 +1,15 @@ package com.emergetools.hackernews.features.bookmarks import android.net.Uri -import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable +import com.emergetools.hackernews.LocalCustomTabsIntent import com.emergetools.hackernews.bookmarkDao -import com.emergetools.hackernews.data.LocalCustomTabsIntent import com.emergetools.hackernews.features.bookmarks.BookmarksDestinations.Bookmarks import kotlinx.serialization.Serializable @@ -48,4 +46,4 @@ fun NavGraphBuilder.bookmarksRoutes( } ) } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsDomain.kt b/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsDomain.kt index ec4086ee..e9987668 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsDomain.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsDomain.kt @@ -3,14 +3,14 @@ package com.emergetools.hackernews.features.comments import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.emergetools.hackernews.data.CommentFormData -import com.emergetools.hackernews.data.CommentInfo -import com.emergetools.hackernews.data.HackerNewsSearchClient -import com.emergetools.hackernews.data.HackerNewsWebClient -import com.emergetools.hackernews.data.PostPage -import com.emergetools.hackernews.data.SearchItem -import com.emergetools.hackernews.data.UserStorage +import com.emergetools.hackernews.data.local.UserStorage import com.emergetools.hackernews.data.relativeTimeStamp +import com.emergetools.hackernews.data.remote.CommentFormData +import com.emergetools.hackernews.data.remote.CommentInfo +import com.emergetools.hackernews.data.remote.HackerNewsBaseClient +import com.emergetools.hackernews.data.remote.HackerNewsWebClient +import com.emergetools.hackernews.data.remote.ItemResponse +import com.emergetools.hackernews.data.remote.PostPage import com.emergetools.hackernews.features.login.LoginDestinations import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.ZoneOffset -import java.time.ZonedDateTime sealed interface CommentsState { val headerState: HeaderState @@ -175,7 +174,7 @@ sealed interface CommentsNavigation { class CommentsViewModel( private val itemId: Long, - private val searchClient: HackerNewsSearchClient, + private val baseClient: HackerNewsBaseClient, private val webClient: HackerNewsWebClient, private val userStorage: UserStorage, ) : ViewModel() { @@ -201,25 +200,23 @@ class CommentsViewModel( init { viewModelScope.launch { - val searchResponse = searchClient.getItem(itemId) + val response = baseClient.getItem(itemId) val postPage = webClient.getPostPage(itemId) - if (searchResponse is SearchItem.Success && postPage is PostPage.Success) { + if (response is ItemResponse.Item && postPage is PostPage.Success) { val comments = postPage.commentInfos.map { it.toCommentState() } val loggedIn = !userStorage.getCookie().first().isNullOrEmpty() internalState.update { CommentsState.Content( id = itemId, - title = searchResponse.item.title ?: "", - author = searchResponse.item.author ?: "", - points = searchResponse.item.points ?: 0, + title = response.title ?: "", + author = response.by ?: "", + points = response.score ?: 0, timeLabel = relativeTimeStamp( - epochSeconds = ZonedDateTime - .parse(searchResponse.item.createdAt) - .toEpochSecond() + epochSeconds = response.time ), - body = BodyState(text = searchResponse.item.text), + body = BodyState(text = response.text), loggedIn = loggedIn, upvoted = postPage.postInfo.upvoted, upvoteUrl = postPage.postInfo.upvoteUrl, @@ -370,12 +367,12 @@ class CommentsViewModel( @Suppress("UNCHECKED_CAST") class Factory( private val itemId: Long, - private val searchClient: HackerNewsSearchClient, + private val baseClient: HackerNewsBaseClient, private val webClient: HackerNewsWebClient, private val userStorage: UserStorage, ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return CommentsViewModel(itemId, searchClient, webClient, userStorage) as T + return CommentsViewModel(itemId, baseClient, webClient, userStorage) as T } } } diff --git a/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsRouting.kt b/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsRouting.kt index f0b161a5..af98280f 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsRouting.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsRouting.kt @@ -8,7 +8,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.emergetools.hackernews.searchClient +import com.emergetools.hackernews.baseClient import com.emergetools.hackernews.userStorage import com.emergetools.hackernews.webClient import kotlinx.serialization.Serializable @@ -25,7 +25,7 @@ fun NavGraphBuilder.commentsRoutes(navController: NavController) { val model = viewModel( factory = CommentsViewModel.Factory( itemId = comments.storyId, - searchClient = context.searchClient(), + baseClient = context.baseClient(), webClient = context.webClient(), userStorage = context.userStorage() ) diff --git a/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsScreen.kt b/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsScreen.kt index 59ed35eb..1179a429 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsScreen.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsScreen.kt @@ -59,6 +59,7 @@ import androidx.compose.ui.unit.sp import com.emergetools.hackernews.R import com.emergetools.hackernews.features.stories.MetadataButton import com.emergetools.hackernews.features.stories.MetadataTag +import com.emergetools.hackernews.parseAsHtml import com.emergetools.hackernews.ui.theme.HackerGreen import com.emergetools.hackernews.ui.theme.HackerNewsTheme import com.emergetools.hackernews.ui.theme.HackerOrange @@ -518,34 +519,34 @@ fun ItemHeader( } } if (state.body.text != null) { - Column( - Modifier - .fillMaxWidth() - .wrapContentHeight() - .clip(RoundedCornerShape(8.dp)) - .clickable { onToggleBody(!state.body.collapsed) } - .background(color = MaterialTheme.colorScheme.surface) - .padding(8.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Icon( - modifier = Modifier - .graphicsLayer { - rotationZ = if (state.body.collapsed) 180f else 0f - } - .size(12.dp), - painter = painterResource(R.drawable.ic_collapse), - tint = MaterialTheme.colorScheme.onSurface, - contentDescription = "Expand or Collapse" - ) - Text( - text = state.body.text.parseAsHtml(), - color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.labelSmall, - overflow = TextOverflow.Ellipsis, - maxLines = if (state.body.collapsed) 4 else Int.MAX_VALUE - ) - } + Column( + Modifier + .fillMaxWidth() + .wrapContentHeight() + .clip(RoundedCornerShape(8.dp)) + .clickable { onToggleBody(!state.body.collapsed) } + .background(color = MaterialTheme.colorScheme.surface) + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + modifier = Modifier + .graphicsLayer { + rotationZ = if (state.body.collapsed) 180f else 0f + } + .size(12.dp), + painter = painterResource(R.drawable.ic_collapse), + tint = MaterialTheme.colorScheme.onSurface, + contentDescription = "Expand or Collapse" + ) + Text( + text = state.body.text.parseAsHtml(), + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.labelSmall, + overflow = TextOverflow.Ellipsis, + maxLines = if (state.body.collapsed) 4 else Int.MAX_VALUE + ) + } } } diff --git a/android/app/src/main/java/com/emergetools/hackernews/features/login/LoginDomain.kt b/android/app/src/main/java/com/emergetools/hackernews/features/login/LoginDomain.kt index aeb8e6e1..40bd2ad2 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/features/login/LoginDomain.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/features/login/LoginDomain.kt @@ -3,8 +3,8 @@ package com.emergetools.hackernews.features.login import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.emergetools.hackernews.data.HackerNewsWebClient -import com.emergetools.hackernews.data.LoginResponse +import com.emergetools.hackernews.data.remote.HackerNewsWebClient +import com.emergetools.hackernews.data.remote.LoginResponse import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -85,4 +85,4 @@ class LoginViewModel(private val webClient: HackerNewsWebClient) : ViewModel() { return LoginViewModel(webClient) as T } } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/emergetools/hackernews/features/settings/SettingsDomain.kt b/android/app/src/main/java/com/emergetools/hackernews/features/settings/SettingsDomain.kt index 2a3b9819..b3f2af78 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/features/settings/SettingsDomain.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/features/settings/SettingsDomain.kt @@ -3,14 +3,13 @@ package com.emergetools.hackernews.features.settings import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.emergetools.hackernews.data.UserStorage +import com.emergetools.hackernews.data.local.UserStorage import com.emergetools.hackernews.features.login.LoginDestinations import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch data class SettingsState( @@ -19,13 +18,14 @@ data class SettingsState( sealed interface SettingsAction { data object LoginPressed : SettingsAction - data object LogoutPressed: SettingsAction + data object LogoutPressed : SettingsAction } sealed interface SettingsNavigation { data object GoToLogin : SettingsNavigation { val login = LoginDestinations.Login } + data class GoToSettingsLink(val url: String) : SettingsNavigation } @@ -57,7 +57,7 @@ class SettingsViewModel(private val userStorage: UserStorage) : ViewModel() { } @Suppress("UNCHECKED_CAST") - class Factory(private val userStorage: UserStorage): ViewModelProvider.Factory { + class Factory(private val userStorage: UserStorage) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return SettingsViewModel(userStorage) as T } diff --git a/android/app/src/main/java/com/emergetools/hackernews/features/settings/SettingsRouting.kt b/android/app/src/main/java/com/emergetools/hackernews/features/settings/SettingsRouting.kt index 725b0a61..0e37cc5b 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/features/settings/SettingsRouting.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/features/settings/SettingsRouting.kt @@ -8,7 +8,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable -import com.emergetools.hackernews.data.LocalCustomTabsIntent +import com.emergetools.hackernews.LocalCustomTabsIntent import com.emergetools.hackernews.userStorage import kotlinx.serialization.Serializable diff --git a/android/app/src/main/java/com/emergetools/hackernews/features/stories/StoriesDomain.kt b/android/app/src/main/java/com/emergetools/hackernews/features/stories/StoriesDomain.kt index 100be9ef..5217b797 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/features/stories/StoriesDomain.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/features/stories/StoriesDomain.kt @@ -3,13 +3,13 @@ package com.emergetools.hackernews.features.stories import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.emergetools.hackernews.data.BaseResponse.Item -import com.emergetools.hackernews.data.BookmarkDao -import com.emergetools.hackernews.data.FeedIdResponse -import com.emergetools.hackernews.data.ItemRepository -import com.emergetools.hackernews.data.Page -import com.emergetools.hackernews.data.next +import com.emergetools.hackernews.data.local.BookmarkDao import com.emergetools.hackernews.data.relativeTimeStamp +import com.emergetools.hackernews.data.remote.FeedIdResponse +import com.emergetools.hackernews.data.remote.HackerNewsBaseClient +import com.emergetools.hackernews.data.remote.ItemResponse.Item +import com.emergetools.hackernews.data.remote.Page +import com.emergetools.hackernews.data.remote.next import com.emergetools.hackernews.features.bookmarks.toLocalBookmark import com.emergetools.hackernews.features.bookmarks.toStoryItem import com.emergetools.hackernews.features.comments.CommentsDestinations @@ -73,7 +73,7 @@ sealed interface StoriesNavigation { } class StoriesViewModel( - private val itemRepository: ItemRepository, + private val baseClient: HackerNewsBaseClient, private val bookmarkDao: BookmarkDao ) : ViewModel() { private val internalState = MutableStateFlow(StoriesState(stories = emptyList())) @@ -116,7 +116,7 @@ class StoriesViewModel( fetchJob = viewModelScope.launch { internalState.update { it.copy(loading = LoadingState.Loading) } - when (val response = itemRepository.getFeedIds(internalState.value.feed)) { + when (val response = baseClient.getFeedIds(internalState.value.feed)) { is FeedIdResponse.Error -> { delay(500) internalState.update { current -> @@ -164,7 +164,7 @@ class StoriesViewModel( internalState.update { current -> current.copy(loading = LoadingState.Refreshing) } - when (val response = itemRepository.getFeedIds(internalState.value.feed)) { + when (val response = baseClient.getFeedIds(internalState.value.feed)) { is FeedIdResponse.Error -> { delay(500) internalState.update { current -> @@ -257,7 +257,7 @@ class StoriesViewModel( private suspend fun fetchPage(page: Page, onLoading: () -> Unit = {}): List { onLoading() val bookmarks = internalState.value.bookmarks - var newStories = itemRepository + var newStories = baseClient .getPage(page) .map { item -> StoryItem.Content( @@ -280,11 +280,11 @@ class StoriesViewModel( @Suppress("UNCHECKED_CAST") class Factory( - private val itemRepository: ItemRepository, + private val baseClient: HackerNewsBaseClient, private val bookmarkDao: BookmarkDao ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return StoriesViewModel(itemRepository, bookmarkDao) as T + return StoriesViewModel(baseClient, bookmarkDao) as T } } } diff --git a/android/app/src/main/java/com/emergetools/hackernews/features/stories/StoriesRouting.kt b/android/app/src/main/java/com/emergetools/hackernews/features/stories/StoriesRouting.kt index d63027b8..0e76643e 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/features/stories/StoriesRouting.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/features/stories/StoriesRouting.kt @@ -1,11 +1,8 @@ package com.emergetools.hackernews.features.stories import android.net.Uri -import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController @@ -13,11 +10,11 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.navigation import androidx.navigation.toRoute +import com.emergetools.hackernews.LocalCustomTabsIntent +import com.emergetools.hackernews.baseClient import com.emergetools.hackernews.bookmarkDao -import com.emergetools.hackernews.data.LocalCustomTabsIntent import com.emergetools.hackernews.features.stories.StoriesDestinations.Closeup import com.emergetools.hackernews.features.stories.StoriesDestinations.Feed -import com.emergetools.hackernews.itemRepository import kotlinx.serialization.Serializable @Serializable @@ -39,7 +36,7 @@ fun NavGraphBuilder.storiesGraph(navController: NavController) { val model = viewModel( factory = StoriesViewModel.Factory( - itemRepository = context.itemRepository(), + baseClient = context.baseClient(), bookmarkDao = context.bookmarkDao() ) ) @@ -66,4 +63,4 @@ fun NavGraphBuilder.storiesGraph(navController: NavController) { StoryScreen(closeup.url) } } -} \ No newline at end of file +}