diff --git a/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt b/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt index 14d26e0..5cd853c 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/AccountService.kt @@ -7,8 +7,9 @@ import com.aiosman.riderpro.data.api.ApiClient import com.aiosman.riderpro.data.api.ChangePasswordRequestBody import com.aiosman.riderpro.data.api.LoginUserRequestBody import com.aiosman.riderpro.data.api.RegisterRequestBody -import com.aiosman.riderpro.model.MomentEntity +import com.aiosman.riderpro.data.api.UpdateNoticeRequestBody import com.aiosman.riderpro.test.TestDatabase +import com.google.gson.annotations.SerializedName import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody @@ -16,7 +17,18 @@ import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import java.io.File import java.io.IOException +import java.util.Date +data class AccountLikeEntity( + val post: NoticePostEntity, + val user: NoticeUserEntity, + val likeTime: Date, +) +data class AccountFavouriteEntity( + val post: NoticePostEntity, + val user: NoticeUserEntity, + val favoriteTime: Date, +) data class AccountProfileEntity( val id: Int, val followerCount: Int, @@ -28,14 +40,6 @@ data class AccountProfileEntity( val isFollowing: Boolean ) -//{ -// "id": 1, -// "username": "root", -// "nickname": "rider_4351", -// "avatar": "/api/v1/public/default_avatar.jpeg", -// "followingCount": 1, -// "followerCount": 0 -//} data class AccountProfile( val id: Int, val username: String, @@ -59,6 +63,219 @@ data class AccountProfile( } } +data class NoticePostEntity( + val id: Int, + val textContent: String, + val images: List, + val time: Date, +) + +data class NoticePost( + @SerializedName("id") + val id: Int, + @SerializedName("textContent") + val textContent: String, + @SerializedName("images") + val images: List, + @SerializedName("time") + val time: String, +) { + fun toNoticePostEntity(): NoticePostEntity { + return NoticePostEntity( + id = id, + textContent = textContent, + images = images.map { + it.copy( + url = ApiClient.BASE_SERVER + it.url + "?token=${AppStore.token}", + thumbnail = ApiClient.BASE_SERVER + it.thumbnail + "?token=${AppStore.token}", + ) + }, + time = ApiClient.dateFromApiString(time) + ) + } +} + +data class NoticeUserEntity( + val id: Int, + val nickName: String, + val avatar: String, +) + +data class NoticeUser( + @SerializedName("id") + val id: Int, + @SerializedName("nickName") + val nickName: String, + @SerializedName("avatar") + val avatar: String, +) { + fun toNoticeUserEntity(): NoticeUserEntity { + return NoticeUserEntity( + id = id, + nickName = nickName, + avatar = ApiClient.BASE_SERVER + avatar + "?token=${AppStore.token}", + ) + } +} + +data class AccountLike( + @SerializedName("isUnread") + val isUnread: Boolean, + @SerializedName("post") + val post: NoticePost, + @SerializedName("user") + val user: NoticeUser, + @SerializedName("likeTime") + val likeTime: String, +) { + fun toAccountLikeEntity(): AccountLikeEntity { + return AccountLikeEntity( + post = post.toNoticePostEntity(), + user = user.toNoticeUserEntity(), + likeTime = ApiClient.dateFromApiString(likeTime) + ) + } +} + +data class AccountFavourite( + @SerializedName("isUnread") + val isUnread: Boolean, + @SerializedName("post") + val post: NoticePost, + @SerializedName("user") + val user: NoticeUser, + @SerializedName("favoriteTime") + val favouriteTime: String, +) { + fun toAccountFavouriteEntity(): AccountFavouriteEntity { + return AccountFavouriteEntity( + post = post.toNoticePostEntity(), + user = user.toNoticeUserEntity(), + favoriteTime = ApiClient.dateFromApiString(favouriteTime) + ) + } +} + +data class AccountFollow( + @SerializedName("id") + val id: Int, + @SerializedName("username") + val username: String, + @SerializedName("nickname") + val nickname: String, + @SerializedName("avatar") + val avatar: String, + @SerializedName("isUnread") + val isUnread: Boolean, + @SerializedName("userId") + val userId: Int, + @SerializedName("isFollowing") + val isFollowing: Boolean, +) + +//{ +// "likeCount": 0, +// "followCount": 0, +// "favoriteCount": 0 +//} +data class AccountNotice( + @SerializedName("likeCount") + val likeCount: Int, + @SerializedName("followCount") + val followCount: Int, + @SerializedName("favoriteCount") + val favoriteCount: Int, + @SerializedName("commentCount") + val commentCount: Int, +) + + +class LikeItemPagingSource( + private val accountService: AccountService, +) : PagingSource() { + override suspend fun load(params: LoadParams): LoadResult { + return try { + val currentPage = params.key ?: 1 + val likes = accountService.getMyLikeNotice( + page = currentPage, + pageSize = 20, + ) + + LoadResult.Page( + data = likes.list.map { + it.toAccountLikeEntity() + }, + prevKey = if (currentPage == 1) null else currentPage - 1, + nextKey = if (likes.list.isEmpty()) null else likes.page + 1 + ) + } catch (exception: IOException) { + return LoadResult.Error(exception) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition + } +} + +class FavoriteItemPagingSource( + private val accountService: AccountService, +) : PagingSource() { + override suspend fun load(params: LoadParams): LoadResult { + return try { + val currentPage = params.key ?: 1 + val favouriteListContainer = accountService.getMyFavouriteNotice( + page = currentPage, + pageSize = 20, + ) + LoadResult.Page( + data = favouriteListContainer.list.map { + it.toAccountFavouriteEntity() + }, + prevKey = if (currentPage == 1) null else currentPage - 1, + nextKey = if (favouriteListContainer.list.isEmpty()) null else favouriteListContainer.page + 1 + ) + } catch (exception: IOException) { + return LoadResult.Error(exception) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition + } +} + +class FollowItemPagingSource( + private val accountService: AccountService, +) : PagingSource() { + override suspend fun load(params: LoadParams): LoadResult { + return try { + val currentPage = params.key ?: 1 + val followListContainer = accountService.getMyFollowNotice( + page = currentPage, + pageSize = 20, + ) + + LoadResult.Page( + data = followListContainer.list.map { + it.copy( + avatar = ApiClient.BASE_SERVER + it.avatar + "?token=${AppStore.token}", + ) + }, + prevKey = if (currentPage == 1) null else currentPage - 1, + nextKey = if (followListContainer.list.isEmpty()) null else followListContainer.page + 1 + ) + } catch (exception: IOException) { + return LoadResult.Error(exception) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition + } +} + + interface AccountService { suspend fun getMyAccountProfile(): AccountProfileEntity suspend fun getAccountProfileById(id: Int): AccountProfileEntity @@ -69,6 +286,11 @@ interface AccountService { suspend fun updateProfile(avatar: UploadImage?, nickName: String?, bio: String?) suspend fun registerUserWithPassword(loginName: String, password: String) suspend fun changeAccountPassword(oldPassword: String, newPassword: String) + suspend fun getMyLikeNotice(page: Int, pageSize: Int): ListContainer + suspend fun getMyFollowNotice(page: Int, pageSize: Int): ListContainer + suspend fun getMyFavouriteNotice(page: Int, pageSize: Int): ListContainer + suspend fun getMyNoticeInfo(): AccountNotice + suspend fun updateNotice(payload: UpdateNoticeRequestBody) } class TestAccountServiceImpl : AccountService { @@ -130,4 +352,36 @@ class TestAccountServiceImpl : AccountService { override suspend fun changeAccountPassword(oldPassword: String, newPassword: String) { ApiClient.api.changePassword(ChangePasswordRequestBody(oldPassword, newPassword)) } + + override suspend fun getMyLikeNotice(page: Int, pageSize: Int): ListContainer { + val resp = ApiClient.api.getMyLikeNotices(page, pageSize) + val body = resp.body() ?: throw ServiceException("Failed to get account") + return body + } + + override suspend fun getMyFollowNotice(page: Int, pageSize: Int): ListContainer { + val resp = ApiClient.api.getMyFollowNotices(page, pageSize) + val body = resp.body() ?: throw ServiceException("Failed to get account") + return body + } + + override suspend fun getMyFavouriteNotice( + page: Int, + pageSize: Int + ): ListContainer { + val resp = ApiClient.api.getMyFavouriteNotices(page, pageSize) + val body = resp.body() ?: throw ServiceException("Failed to get account") + return body + } + + override suspend fun getMyNoticeInfo(): AccountNotice { + val resp = ApiClient.api.getMyNoticeInfo() + val body = resp.body() ?: throw ServiceException("Failed to get account") + return body.data + } + + override suspend fun updateNotice(payload: UpdateNoticeRequestBody) { + ApiClient.api.updateNoticeInfo(payload) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/data/Base.kt b/app/src/main/java/com/aiosman/riderpro/data/Base.kt index 023efbd..e69de29 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/Base.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/Base.kt @@ -1,2 +0,0 @@ -package com.aiosman.riderpro.data - diff --git a/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt b/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt index 61e99ab..c311e35 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/CommentService.kt @@ -9,27 +9,23 @@ import com.aiosman.riderpro.test.TestDatabase import com.google.gson.annotations.SerializedName import java.io.IOException import java.util.Calendar +import java.util.Date import kotlin.math.min interface CommentService { - suspend fun getComments(pageNumber: Int, postId: Int? = null): ListContainer + suspend fun getComments( + pageNumber: Int, + postId: Int? = null, + postUser: Int? = null, + selfNotice: Boolean? = null + ): ListContainer + suspend fun createComment(postId: Int, content: String) suspend fun likeComment(commentId: Int) suspend fun dislikeComment(commentId: Int) + suspend fun updateReadStatus(commentId: Int) } -//{ -// "id": 2, -// "content": "123", -// "User": { -// "id": 1, -// "nickName": "", -// "avatar": "/api/v1/public/default_avatar.jpeg" -//}, -// "likeCount": 1, -// "isLiked": true, -// "createdAt": "2024-08-05 02:53:48" -//} data class Comment( @SerializedName("id") val id: Int, @@ -42,20 +38,37 @@ data class Comment( @SerializedName("isLiked") val isLiked: Boolean, @SerializedName("createdAt") - val createdAt: String + val createdAt: String, + @SerializedName("postId") + val postId: Int, + @SerializedName("post") + val post: NoticePost?, + @SerializedName("isUnread") + val isUnread: Boolean ) { fun toCommentEntity(): CommentEntity { return CommentEntity( id = id, name = user.nickName, comment = content, - date = createdAt, + date = ApiClient.dateFromApiString(createdAt), likes = likeCount, replies = emptyList(), - postId = 0, + postId = postId, avatar = ApiClient.BASE_SERVER + user.avatar + "?token=${AppStore.token}", author = user.id, - liked = isLiked + liked = isLiked, + unread = isUnread, + post = post?.let { + it.copy( + images = it.images.map { + it.copy( + url = ApiClient.BASE_SERVER + it.url + "?token=${AppStore.token}", + thumbnail = ApiClient.BASE_SERVER + it.thumbnail + "?token=${AppStore.token}" + ) + } + ) + } ) } } @@ -64,25 +77,31 @@ data class CommentEntity( val id: Int, val name: String, val comment: String, - val date: String, + val date: Date, val likes: Int, val replies: List, val postId: Int = 0, val avatar: String, val author: Long, var liked: Boolean, + var unread: Boolean = false, + var post: NoticePost? ) class CommentPagingSource( private val remoteDataSource: CommentRemoteDataSource, - private val postId: Int? = null + private val postId: Int? = null, + private val postUser: Int? = null, + private val selfNotice: Boolean? = null ) : PagingSource() { override suspend fun load(params: LoadParams): LoadResult { return try { val currentPage = params.key ?: 1 val comments = remoteDataSource.getComments( pageNumber = currentPage, - postId = postId + postId = postId, + postUser = postUser, + selfNotice = selfNotice ) LoadResult.Page( data = comments.list, @@ -103,15 +122,37 @@ class CommentPagingSource( class CommentRemoteDataSource( private val commentService: CommentService, ) { - suspend fun getComments(pageNumber: Int, postId: Int?): ListContainer { - return commentService.getComments(pageNumber, postId) + suspend fun getComments( + pageNumber: Int, + postId: Int?, + postUser: Int?, + selfNotice: Boolean? + ): ListContainer { + return commentService.getComments( + pageNumber, + postId, + postUser = postUser, + selfNotice = selfNotice + ) } } class TestCommentServiceImpl : CommentService { - override suspend fun getComments(pageNumber: Int, postId: Int?): ListContainer { - val resp = ApiClient.api.getComments(pageNumber, postId) + override suspend fun getComments( + pageNumber: Int, + postId: Int?, + postUser: Int?, + selfNotice: Boolean? + ): ListContainer { + val resp = ApiClient.api.getComments( + pageNumber, + postId, + postUser = postUser, + selfNotice = selfNotice?.let { + if (it) 1 else 0 + } + ) val body = resp.body() ?: throw ServiceException("Failed to get comments") return ListContainer( list = body.list.map { it.toCommentEntity() }, @@ -136,6 +177,11 @@ class TestCommentServiceImpl : CommentService { return } + override suspend fun updateReadStatus(commentId: Int) { + val resp = ApiClient.api.updateReadStatus(commentId) + return + } + companion object { const val DataBatchSize = 5 } diff --git a/app/src/main/java/com/aiosman/riderpro/data/api/ApiClient.kt b/app/src/main/java/com/aiosman/riderpro/data/api/ApiClient.kt index 2eebf3f..0487442 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/api/ApiClient.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/api/ApiClient.kt @@ -1,5 +1,6 @@ package com.aiosman.riderpro.data.api +import android.icu.text.SimpleDateFormat import com.aiosman.riderpro.AppStore import com.aiosman.riderpro.ConstVars import okhttp3.Interceptor @@ -8,6 +9,8 @@ import okhttp3.Response import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.security.cert.CertificateException +import java.util.Date +import java.util.Locale import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager @@ -17,10 +20,18 @@ fun getUnsafeOkHttpClient(): OkHttpClient { // Create a trust manager that does not validate certificate chains val trustAllCerts = arrayOf(object : X509TrustManager { @Throws(CertificateException::class) - override fun checkClientTrusted(chain: Array, authType: String) {} + override fun checkClientTrusted( + chain: Array, + authType: String + ) { + } @Throws(CertificateException::class) - override fun checkServerTrusted(chain: Array, authType: String) {} + override fun checkServerTrusted( + chain: Array, + authType: String + ) { + } override fun getAcceptedIssuers(): Array = arrayOf() }) @@ -40,6 +51,7 @@ fun getUnsafeOkHttpClient(): OkHttpClient { throw RuntimeException(e) } } + class AuthInterceptor() : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val requestBuilder = chain.request().newBuilder() @@ -52,6 +64,7 @@ object ApiClient { const val BASE_SERVER = ConstVars.BASE_SERVER const val BASE_API_URL = "${BASE_SERVER}/api/v1" const val RETROFIT_URL = "${BASE_API_URL}/" + const val TIME_FORMAT = "yyyy-MM-dd HH:mm:ss" private val okHttpClient: OkHttpClient by lazy { getUnsafeOkHttpClient() } @@ -62,9 +75,19 @@ object ApiClient { .addConverterFactory(GsonConverterFactory.create()) .build() } - val api: RiderProAPI by lazy { retrofit.create(RiderProAPI::class.java) } + fun formatTime(date: Date): String { + val dateFormat = SimpleDateFormat(TIME_FORMAT, Locale.getDefault()) + return dateFormat.format(date) + } + + fun dateFromApiString(apiString: String): Date { + val timeFormat = ApiClient.TIME_FORMAT + val simpleDateFormat = SimpleDateFormat(timeFormat, Locale.getDefault()) + val date = simpleDateFormat.parse(apiString) + return date + } } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/data/api/RiderProAPI.kt b/app/src/main/java/com/aiosman/riderpro/data/api/RiderProAPI.kt index 28c39cb..b581edb 100644 --- a/app/src/main/java/com/aiosman/riderpro/data/api/RiderProAPI.kt +++ b/app/src/main/java/com/aiosman/riderpro/data/api/RiderProAPI.kt @@ -1,5 +1,9 @@ package com.aiosman.riderpro.data.api +import com.aiosman.riderpro.data.AccountFavourite +import com.aiosman.riderpro.data.AccountFollow +import com.aiosman.riderpro.data.AccountLike +import com.aiosman.riderpro.data.AccountNotice import com.aiosman.riderpro.data.AccountProfile import com.aiosman.riderpro.data.AccountProfileEntity import com.aiosman.riderpro.data.Comment @@ -59,6 +63,15 @@ data class ChangePasswordRequestBody( val newPassword: String = "" ) +data class UpdateNoticeRequestBody( + @SerializedName("lastLookLikeTime") + val lastLookLikeTime: String? = null, + @SerializedName("lastLookFollowTime") + val lastLookFollowTime: String? = null, + @SerializedName("lastLookFavoriteTime") + val lastLookFavouriteTime: String? = null +) + interface RiderProAPI { @POST("register") suspend fun register(@Body body: RegisterRequestBody): Response @@ -76,6 +89,7 @@ interface RiderProAPI { @Query("timelineId") timelineId: Int? = null, @Query("authorId") authorId: Int? = null, @Query("contentSearch") contentSearch: String? = null, + @Query("postUser") postUser: Int? = null, ): Response> @Multipart @@ -126,12 +140,19 @@ interface RiderProAPI { @Path("id") id: Int ): Response + @POST("comment/{id}/read") + suspend fun updateReadStatus( + @Path("id") id: Int + ): Response + @GET("comments") suspend fun getComments( @Query("page") page: Int = 1, @Query("postId") postId: Int? = null, @Query("pageSize") pageSize: Int = 20, + @Query("postUser") postUser: Int? = null, + @Query("selfNotice") selfNotice: Int? = 0, ): Response> @GET("account/my") @@ -149,6 +170,33 @@ interface RiderProAPI { @Body body: ChangePasswordRequestBody ): Response + @GET("account/my/notice/like") + suspend fun getMyLikeNotices( + @Query("page") page: Int = 1, + @Query("pageSize") pageSize: Int = 20, + ): Response> + + @GET("account/my/notice/follow") + suspend fun getMyFollowNotices( + @Query("page") page: Int = 1, + @Query("pageSize") pageSize: Int = 20, + ): Response> + + @GET("account/my/notice/favourite") + suspend fun getMyFavouriteNotices( + @Query("page") page: Int = 1, + @Query("pageSize") pageSize: Int = 20, + ): Response> + + @GET("account/my/notice") + suspend fun getMyNoticeInfo(): Response> + + @POST("account/my/notice") + suspend fun updateNoticeInfo( + @Body body: UpdateNoticeRequestBody + ): Response + + @GET("profile/{id}") suspend fun getAccountProfileById( @Path("id") id: Int diff --git a/app/src/main/java/com/aiosman/riderpro/exp/Date.kt b/app/src/main/java/com/aiosman/riderpro/exp/Date.kt new file mode 100644 index 0000000..3d7e429 --- /dev/null +++ b/app/src/main/java/com/aiosman/riderpro/exp/Date.kt @@ -0,0 +1,25 @@ +package com.aiosman.riderpro.exp + +import android.icu.text.SimpleDateFormat +import com.aiosman.riderpro.data.api.ApiClient +import java.util.Date +import java.util.Locale + +fun Date.timeAgo(): String { + val now = Date() + val diffInMillis = now.time - this.time + + val seconds = diffInMillis / 1000 + val minutes = seconds / 60 + val hours = minutes / 60 + val days = hours / 24 + val years = days / 365 + + return when { + seconds < 60 -> "$seconds seconds ago" + minutes < 60 -> "$minutes minutes ago" + hours < 24 -> "$hours hours ago" + days < 365 -> "$days days ago" + else -> "$years years ago" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/test/TestDatabase.kt b/app/src/main/java/com/aiosman/riderpro/test/TestDatabase.kt index 94ca734..0fe761f 100644 --- a/app/src/main/java/com/aiosman/riderpro/test/TestDatabase.kt +++ b/app/src/main/java/com/aiosman/riderpro/test/TestDatabase.kt @@ -9,6 +9,7 @@ import com.google.gson.Gson import com.google.gson.GsonBuilder import io.github.serpro69.kfaker.faker import java.io.File +import java.util.Calendar object TestDatabase { var momentData = emptyList() @@ -90,14 +91,16 @@ object TestDatabase { var newCommentEntity = CommentEntity( name = commentPerson.nickName, comment = "this is comment ${commentIdCounter}", - date = "2023-02-02 11:23", + date = Calendar.getInstance().time, likes = 0, replies = emptyList(), postId = momentIdCounter, avatar = commentPerson.avatar, author = commentPerson.id.toLong(), id = commentIdCounter, - liked = false + liked = false, + unread = false, + post = null ) // generate like comment list for (likeIdx in 0..faker.random.nextInt(0, 5)) { diff --git a/app/src/main/java/com/aiosman/riderpro/ui/Navi.kt b/app/src/main/java/com/aiosman/riderpro/ui/Navi.kt index 30a85f8..d1ffb78 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/Navi.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/Navi.kt @@ -25,6 +25,7 @@ import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.LocalSharedTransitionScope import com.aiosman.riderpro.ui.account.AccountEditScreen import com.aiosman.riderpro.ui.comment.CommentsScreen +import com.aiosman.riderpro.ui.favourite.FavouriteScreen import com.aiosman.riderpro.ui.follower.FollowerScreen import com.aiosman.riderpro.ui.gallery.OfficialGalleryScreen import com.aiosman.riderpro.ui.gallery.OfficialPhotographerScreen @@ -66,6 +67,7 @@ sealed class NavigationRoute( data object AccountEdit : NavigationRoute("AccountEditScreen") data object ImageViewer : NavigationRoute("ImageViewer") data object ChangePasswordScreen : NavigationRoute("ChangePasswordScreen") + data object FavouritesScreen : NavigationRoute("FavouritesScreen") } @@ -189,6 +191,9 @@ fun NavigationController( composable(route = NavigationRoute.ChangePasswordScreen.route) { ChangePasswordScreen() } + composable(route = NavigationRoute.FavouritesScreen.route) { + FavouriteScreen() + } } diff --git a/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouritePageViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouritePageViewModel.kt new file mode 100644 index 0000000..237fa4a --- /dev/null +++ b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouritePageViewModel.kt @@ -0,0 +1,53 @@ +package com.aiosman.riderpro.ui.favourite + +import android.icu.util.Calendar +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn +import com.aiosman.riderpro.data.AccountFavourite +import com.aiosman.riderpro.data.AccountFavouriteEntity +import com.aiosman.riderpro.data.AccountLike +import com.aiosman.riderpro.data.AccountService +import com.aiosman.riderpro.data.FavoriteItemPagingSource +import com.aiosman.riderpro.data.LikeItemPagingSource +import com.aiosman.riderpro.data.TestAccountServiceImpl +import com.aiosman.riderpro.data.api.ApiClient +import com.aiosman.riderpro.data.api.UpdateNoticeRequestBody +import com.aiosman.riderpro.ui.like.LikePageViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +object FavouritePageViewModel : ViewModel() { + private val accountService: AccountService = TestAccountServiceImpl() + private val _favouriteItemsFlow = + MutableStateFlow>(PagingData.empty()) + val favouriteItemsFlow = _favouriteItemsFlow.asStateFlow() + + init { + viewModelScope.launch { + Pager( + config = PagingConfig(pageSize = 5, enablePlaceholders = false), + pagingSourceFactory = { + FavoriteItemPagingSource( + accountService + ) + } + ).flow.cachedIn(viewModelScope).collectLatest { + _favouriteItemsFlow.value = it + } + } + } + suspend fun updateNotice() { + var now = Calendar.getInstance().time + accountService.updateNotice( + UpdateNoticeRequestBody( + lastLookFavouriteTime = ApiClient.formatTime(now) + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteScreen.kt b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteScreen.kt new file mode 100644 index 0000000..bc15a0a --- /dev/null +++ b/app/src/main/java/com/aiosman/riderpro/ui/favourite/FavouriteScreen.kt @@ -0,0 +1,93 @@ +package com.aiosman.riderpro.ui.favourite + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.paging.compose.collectAsLazyPagingItems +import com.aiosman.riderpro.LocalNavController +import com.aiosman.riderpro.R +import com.aiosman.riderpro.ui.comment.NoticeScreenHeader +import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout +import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder +import com.aiosman.riderpro.ui.like.ActionNoticeItem +import com.aiosman.riderpro.ui.like.LikePageViewModel + +@Preview +@Composable +fun FavouriteScreen() { + val model = FavouritePageViewModel + val coroutineScope = rememberCoroutineScope() + val listState = rememberLazyListState() + var dataFlow = model.favouriteItemsFlow + var favourites = dataFlow.collectAsLazyPagingItems() + LaunchedEffect(Unit) { + model.updateNotice() + } + StatusBarMaskLayout( + darkIcons = true, + maskBoxBackgroundColor = Color(0xFFFFFFFF) + ) { + Column( + modifier = Modifier + .weight(1f) + .background(color = Color(0xFFFFFFFF)) + .padding(horizontal = 16.dp) + ) { + NoticeScreenHeader( + "FAVOURITE", + moreIcon = false + ) + Spacer(modifier = Modifier.height(28.dp)) + LazyColumn( + modifier = Modifier.weight(1f), + state = listState, + ) { + items(favourites.itemCount) { + val favouriteItem = favourites[it] + if (favouriteItem != null) { + ActionNoticeItem( + avatar = favouriteItem.user.avatar, + nickName = favouriteItem.user.nickName, + likeTime = favouriteItem.favoriteTime, + thumbnail = favouriteItem.post.images[0].thumbnail, + action = "favourite", + userId = favouriteItem.user.id, + postId = favouriteItem.post.id, + ) + } + } + item { + BottomNavigationPlaceholder() + } + } + + + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerPage.kt b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerPage.kt index 86ec0fd..b60f72e 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerPage.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerPage.kt @@ -13,32 +13,53 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.paging.compose.collectAsLazyPagingItems +import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.R +import com.aiosman.riderpro.data.AccountFollow +import com.aiosman.riderpro.ui.NavigationRoute import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout import com.aiosman.riderpro.ui.comment.NoticeScreenHeader +import com.aiosman.riderpro.ui.composables.CustomAsyncImage +import com.aiosman.riderpro.ui.modifiers.noRippleClickable +import kotlinx.coroutines.launch @Preview @Composable fun FollowerScreen() { + val scope = rememberCoroutineScope() StatusBarMaskLayout( modifier = Modifier.padding(horizontal = 16.dp) ) { + val model = FollowerViewModel + var dataFlow = model.followerItemsFlow + var followers = dataFlow.collectAsLazyPagingItems() NoticeScreenHeader("FOLLOWERS") Spacer(modifier = Modifier.height(28.dp)) + LaunchedEffect(Unit) { + model.updateNotice() + } LazyColumn( modifier = Modifier.weight(1f) ) { - item { - repeat(20) { - FollowerItem() + items(followers.itemCount) { index -> + followers[index]?.let { follower -> + FollowerItem(follower) { + scope.launch { + model.followUser(follower.userId) + } + } } } } @@ -47,7 +68,12 @@ fun FollowerScreen() { @Composable -fun FollowerItem() { +fun FollowerItem( + item: AccountFollow, + onFollow: () -> Unit = {} +) { + val context = LocalContext.current + val navController = LocalNavController.current Box( modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) ) { @@ -55,37 +81,52 @@ fun FollowerItem() { modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { - Image( - painter = painterResource(id = R.drawable.default_avatar), - contentDescription = "Follower", - modifier = Modifier.size(40.dp) + CustomAsyncImage( + context = context, + imageUrl = item.avatar, + contentDescription = item.nickname, + modifier = Modifier + .size(40.dp) + .noRippleClickable { + navController.navigate( + NavigationRoute.AccountProfile.route.replace( + "{id}", + item.userId.toString() + ) + ) + } ) Spacer(modifier = Modifier.width(12.dp)) Column( modifier = Modifier.weight(1f) ) { - Text("Username", fontWeight = FontWeight.Bold, fontSize = 16.sp) - Spacer(modifier = Modifier.height(5.dp)) - Text("Username", fontSize = 12.sp, color = Color(0x99000000)) + Text(item.nickname, fontWeight = FontWeight.Bold, fontSize = 16.sp) } - Box { - Image( - painter = painterResource(id = R.drawable.follow_bg), - contentDescription = "Like", - modifier = Modifier - .width(79.dp) - .height(24.dp) - ) - Text( - "FOLLOW", - fontSize = 14.sp, - color = Color(0xFFFFFFFF), - modifier = Modifier.align( - Alignment.Center + if (!item.isFollowing) { + Box( + modifier = Modifier.noRippleClickable { + onFollow() + } + ) { + Image( + painter = painterResource(id = R.drawable.follow_bg), + contentDescription = "Follow", + modifier = Modifier + .width(79.dp) + .height(24.dp) ) - ) + Text( + "FOLLOW", + fontSize = 14.sp, + color = Color(0xFFFFFFFF), + modifier = Modifier.align( + Alignment.Center + ) + ) + } } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerViewModel.kt new file mode 100644 index 0000000..850d5ea --- /dev/null +++ b/app/src/main/java/com/aiosman/riderpro/ui/follower/FollowerViewModel.kt @@ -0,0 +1,70 @@ +package com.aiosman.riderpro.ui.follower + +import android.icu.util.Calendar +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn +import androidx.paging.map +import com.aiosman.riderpro.data.AccountFollow +import com.aiosman.riderpro.data.AccountService +import com.aiosman.riderpro.data.FollowItemPagingSource +import com.aiosman.riderpro.data.TestAccountServiceImpl +import com.aiosman.riderpro.data.TestUserServiceImpl +import com.aiosman.riderpro.data.UserService +import com.aiosman.riderpro.data.api.ApiClient +import com.aiosman.riderpro.data.api.UpdateNoticeRequestBody +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +object FollowerViewModel : ViewModel() { + private val accountService: AccountService = TestAccountServiceImpl() + private val userService: UserService = TestUserServiceImpl() + private val _followerItemsFlow = + MutableStateFlow>(PagingData.empty()) + val followerItemsFlow = _followerItemsFlow.asStateFlow() + + init { + viewModelScope.launch { + Pager( + config = PagingConfig(pageSize = 5, enablePlaceholders = false), + pagingSourceFactory = { + FollowItemPagingSource( + accountService + ) + } + ).flow.cachedIn(viewModelScope).collectLatest { + _followerItemsFlow.value = it + } + } + } + + private fun updateIsFollow(id: Int) { + val currentPagingData = _followerItemsFlow.value + val updatedPagingData = currentPagingData.map { follow -> + if (follow.userId == id) { + follow.copy(isFollowing = true) + } else { + follow + } + } + _followerItemsFlow.value = updatedPagingData + } + suspend fun followUser(userId: Int) { + userService.followUser(userId.toString()) + updateIsFollow(userId) + } + + suspend fun updateNotice() { + var now = Calendar.getInstance().time + accountService.updateNotice( + UpdateNoticeRequestBody( + lastLookFollowTime = ApiClient.formatTime(now) + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/like/LikePage.kt b/app/src/main/java/com/aiosman/riderpro/ui/like/LikePage.kt index 414c739..62d69b0 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/like/LikePage.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/like/LikePage.kt @@ -1,7 +1,7 @@ package com.aiosman.riderpro.ui.like -import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -12,47 +12,40 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.aiosman.riderpro.R +import androidx.paging.compose.collectAsLazyPagingItems +import com.aiosman.riderpro.LocalNavController +import com.aiosman.riderpro.data.AccountLike +import com.aiosman.riderpro.exp.timeAgo +import com.aiosman.riderpro.ui.NavigationRoute import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout import com.aiosman.riderpro.ui.comment.NoticeScreenHeader import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder +import com.aiosman.riderpro.ui.composables.CustomAsyncImage +import com.aiosman.riderpro.ui.modifiers.noRippleClickable +import java.util.Date @Preview @Composable fun LikeScreen() { val model = LikePageViewModel - val coroutineScope = rememberCoroutineScope() val listState = rememberLazyListState() - - // observe list scrolling - val reachedBottom: Boolean by remember { - derivedStateOf { - val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull() - lastVisibleItem?.index != 0 && lastVisibleItem?.index == listState.layoutInfo.totalItemsCount - 3 - } - } + var dataFlow = model.likeItemsFlow + var likes = dataFlow.collectAsLazyPagingItems() LaunchedEffect(Unit) { - LikePageViewModel.loader.loadData() - } - LaunchedEffect(reachedBottom) { - if (reachedBottom) LikePageViewModel.loader.loadMore() + model.updateNotice() } StatusBarMaskLayout( darkIcons = true, @@ -64,69 +57,97 @@ fun LikeScreen() { .background(color = Color(0xFFFFFFFF)) .padding(horizontal = 16.dp) ) { - NoticeScreenHeader("LIKES") + NoticeScreenHeader( + "LIKES", + moreIcon = false + ) Spacer(modifier = Modifier.height(28.dp)) LazyColumn( modifier = Modifier.weight(1f), state = listState, - - ) { - items(LikePageViewModel.loader.list, key = { it.id }) { - LikeItem(it) + ) { + items(likes.itemCount) { + val likeItem = likes[it] + if (likeItem != null) { + ActionNoticeItem( + avatar = likeItem.user.avatar, + nickName = likeItem.user.nickName, + likeTime = likeItem.likeTime, + thumbnail = likeItem.post.images[0].thumbnail, + action = "like", + userId = likeItem.user.id, + postId = likeItem.post.id + ) + } } item { BottomNavigationPlaceholder() } } - - } } - - } @Composable -fun LikeItem(itemData: LikeItemData) { +fun ActionNoticeItem( + avatar: String, + nickName: String, + likeTime: Date, + thumbnail: String, + action: String, + userId: Int, + postId: Int +) { + val context = LocalContext.current + val navController = LocalNavController.current Box( modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) ) { Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.Top, ) { - Image( - painter = painterResource(id = R.drawable.default_avatar), - contentDescription = "Like", - modifier = Modifier.size(40.dp) + CustomAsyncImage( + context, + imageUrl = avatar, + modifier = Modifier + .size(40.dp) + .noRippleClickable { + navController.navigate( + NavigationRoute.AccountProfile.route.replace( + "{id}", + userId.toString() + ) + ) + }, + contentDescription = action, ) Spacer(modifier = Modifier.width(12.dp)) Column( - modifier = Modifier.weight(1f) + modifier = Modifier + .weight(1f) + .noRippleClickable { + navController.navigate( + NavigationRoute.Post.route.replace( + "{id}", + postId.toString() + ) + ) + } ) { - Text(itemData.name, fontWeight = FontWeight.Bold, fontSize = 16.sp) + Text(nickName, fontWeight = FontWeight.Bold, fontSize = 16.sp) Spacer(modifier = Modifier.height(5.dp)) - Text("Username", fontSize = 12.sp, color = Color(0x99000000)) + Row { + Text(likeTime.timeAgo(), fontSize = 12.sp, color = Color(0x99000000)) + } } - Box { - Image( - painter = painterResource(id = R.drawable.follow_bg), - contentDescription = "Like", - modifier = Modifier - .width(79.dp) - .height(24.dp) - ) - Text( - "FOLLOW", - fontSize = 14.sp, - color = Color(0xFFFFFFFF), - modifier = Modifier.align( - Alignment.Center - ) - ) - } - + CustomAsyncImage( + context, + imageUrl = thumbnail, + modifier = Modifier.size(64.dp), + contentDescription = action, + ) } } } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/like/LikePageViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/like/LikePageViewModel.kt index 4e70301..adf64c3 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/like/LikePageViewModel.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/like/LikePageViewModel.kt @@ -1,68 +1,51 @@ package com.aiosman.riderpro.ui.like -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue +import android.icu.util.Calendar import androidx.lifecycle.ViewModel -import com.aiosman.riderpro.test.MockDataContainer -import com.aiosman.riderpro.test.MockDataSource -import com.aiosman.riderpro.test.MockListContainer +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn +import com.aiosman.riderpro.data.AccountLike +import com.aiosman.riderpro.data.AccountLikeEntity +import com.aiosman.riderpro.data.AccountService +import com.aiosman.riderpro.data.LikeItemPagingSource +import com.aiosman.riderpro.data.TestAccountServiceImpl +import com.aiosman.riderpro.data.api.ApiClient +import com.aiosman.riderpro.data.api.UpdateNoticeRequestBody +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch -class LikeDataSource : MockDataSource() { - init { - var newList = mutableListOf() - for (i in 0..999) { - newList.add(LikeItemData(i, "Like $i")) - } - list = newList - } -} -abstract class DataLoader { - var list = mutableListOf() - var page by mutableStateOf(1) - var pageSize by mutableStateOf(20) - var total by mutableStateOf(0) - var noMoreData by mutableStateOf(false) - suspend fun loadData() { - val resp = fetchData(page, pageSize) - if (resp.success) { - resp.data?.let { - total = it.total - list = it.list.toMutableList() - if (it.list.size < pageSize) { - noMoreData = true - } - } - } - } - suspend fun loadMore(){ - if (list.size >= total) return - page++ - val resp = fetchData(page, pageSize) - if (resp.success) { - resp.data?.let { - total = it.total - list.addAll(it.list) - if (it.list.size < pageSize) { - noMoreData = true - } - } - } - } - abstract suspend fun fetchData(page: Int, pageSize: Int): MockDataContainer> -} -class LikeListLoader : DataLoader() { - var mockDataSource = LikeDataSource() - override suspend fun fetchData(page: Int, pageSize: Int): MockDataContainer> { - return mockDataSource.fetchData(page, pageSize) - } -} object LikePageViewModel : ViewModel() { - val loader = LikeListLoader() -} + private val accountService: AccountService = TestAccountServiceImpl() + private val _likeItemsFlow = MutableStateFlow>(PagingData.empty()) + val likeItemsFlow = _likeItemsFlow.asStateFlow() -data class LikeItemData( - val id: Int, - val name: String, -) \ No newline at end of file + init { + viewModelScope.launch { + Pager( + config = PagingConfig(pageSize = 5, enablePlaceholders = false), + pagingSourceFactory = { + LikeItemPagingSource( + accountService + ) + } + ).flow.cachedIn(viewModelScope).collectLatest { + _likeItemsFlow.value = it + } + } + } + + suspend fun updateNotice() { + var now = Calendar.getInstance().time + accountService.updateNotice( + UpdateNoticeRequestBody( + lastLookLikeTime = ApiClient.formatTime(now) + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/message/MessageList.kt b/app/src/main/java/com/aiosman/riderpro/ui/message/MessageList.kt index bc91fa0..ddc3380 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/message/MessageList.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/message/MessageList.kt @@ -2,7 +2,6 @@ package com.aiosman.riderpro.ui.message import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -24,29 +23,43 @@ 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.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.paging.compose.collectAsLazyPagingItems import com.aiosman.riderpro.LocalNavController import com.aiosman.riderpro.R +import com.aiosman.riderpro.data.Comment +import com.aiosman.riderpro.data.CommentEntity +import com.aiosman.riderpro.exp.timeAgo +import com.aiosman.riderpro.ui.NavigationRoute import com.aiosman.riderpro.ui.composables.StatusBarMaskLayout import com.aiosman.riderpro.ui.composables.BottomNavigationPlaceholder +import com.aiosman.riderpro.ui.composables.CustomAsyncImage +import com.aiosman.riderpro.ui.modifiers.noRippleClickable import com.google.accompanist.systemuicontroller.rememberSystemUiController @Preview(showBackground = true) @Composable fun NotificationsScreen() { + val model = MessageListViewModel val navController = LocalNavController.current val systemUiController = rememberSystemUiController() + var dataFlow = model.commentItemsFlow + var comments = dataFlow.collectAsLazyPagingItems() LaunchedEffect(Unit) { systemUiController.setNavigationBarColor(Color.Transparent) + model.initData() } StatusBarMaskLayout(darkIcons = true) { Column( - modifier = Modifier.fillMaxWidth().weight(1f) + modifier = Modifier + .fillMaxWidth() + .weight(1f) ) { Box( modifier = Modifier @@ -65,42 +78,47 @@ fun NotificationsScreen() { .padding(horizontal = 16.dp), horizontalArrangement = Arrangement.SpaceBetween, ) { - NotificationIndicator(10, R.drawable.rider_pro_like, "LIKE") { - navController.navigate("Likes") + NotificationIndicator(model.likeNoticeCount, R.drawable.rider_pro_like, "LIKE") { + navController.navigate(NavigationRoute.Likes.route) } - NotificationIndicator(10, R.drawable.rider_pro_followers, "FOLLOWERS"){ - navController.navigate("Followers") + NotificationIndicator( + model.followNoticeCount, + R.drawable.rider_pro_followers, + "FOLLOWERS" + ) { + navController.navigate(NavigationRoute.Followers.route) } - NotificationIndicator(10, R.drawable.rider_pro_comments, "COMMENTS"){ - navController.navigate("Comments") - + NotificationIndicator( + model.favouriteNoticeCount, + R.drawable.rider_pro_favoriate, + "Favourites" + ) { + navController.navigate(NavigationRoute.FavouritesScreen.route) } } HorizontalDivider(color = Color(0xFFEbEbEb), modifier = Modifier.padding(16.dp)) - NotificationCounterItem(24) + NotificationCounterItem(model.commentNoticeCount) LazyColumn( modifier = Modifier .weight(1f) .fillMaxSize() ) { - item { - repeat(20) { - MessageItem( - MessageItemData( - userName = "Onyama Limba", - message = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", - timeAgo = "3 days ago", - profileImage = R.drawable.default_avatar + items(comments.itemCount) { index -> + comments[index]?.let { comment -> + CommentItem(comment) { + model.updateReadStatus(comment.id) + navController.navigate( + NavigationRoute.Post.route.replace( + "{id}", + comment.postId.toString() + ) ) - ) - + } } - BottomNavigationPlaceholder() - - } - - + item { + BottomNavigationPlaceholder() + } } } } @@ -121,7 +139,7 @@ fun NotificationIndicator( modifier = Modifier .padding(16.dp) .align(Alignment.TopCenter) - .clickable { + .noRippleClickable { onClick() } ) { @@ -204,68 +222,101 @@ fun NotificationCounterItem(count: Int) { } } -data class MessageItemData( - val userName: String, - val message: String, - val timeAgo: String, - val profileImage: Int -) @Composable -fun MessageItem(messageItemData: MessageItemData) { +fun CommentItem( + commentItem: CommentEntity, + onPostClick: () -> Unit = {}, +) { + val navController = LocalNavController.current + val context = LocalContext.current Row( modifier = Modifier.padding(16.dp) ) { - Box() { - Image( - painter = painterResource(id = R.drawable.default_avatar), - contentDescription = "", + Box { + CustomAsyncImage( + context = context, + imageUrl = commentItem.avatar, + contentDescription = commentItem.name, modifier = Modifier .size(48.dp) .clip(RoundedCornerShape(4.dp)) + .noRippleClickable { + navController.navigate( + NavigationRoute.AccountProfile.route.replace( + "{id}", + commentItem.author.toString() + ) + ) + } ) } - Column( - modifier = Modifier.weight(1f) + Row( + modifier = Modifier + .weight(1f) + .padding(start = 16.dp) + .noRippleClickable { + onPostClick() + } ) { - Text( - text = "Onyama Limba", - fontSize = 16.sp, - modifier = Modifier.padding(start = 16.dp) - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", - fontSize = 14.sp, - modifier = Modifier.padding(start = 16.dp), - maxLines = 1, - color = Color(0x99000000) - ) - } -// Spacer(modifier = Modifier.weight(1f)) - Column( - horizontalAlignment = Alignment.End - ) { - Text( - text = "3 days ago", - fontSize = 14.sp, - color = Color(0x66000000) - ) - Spacer(modifier = Modifier.height(6.dp)) - Box( - modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .background(Color(0xFFE53935)) - .padding(4.dp) + Column( + modifier = Modifier.weight(1f) ) { Text( - text = "24", - color = Color.White, - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.align(Alignment.Center) + text = commentItem.name, + fontSize = 16.sp, + modifier = Modifier.padding(start = 16.dp) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = commentItem.comment, + fontSize = 14.sp, + modifier = Modifier.padding(start = 16.dp), + maxLines = 1, + color = Color(0x99000000) ) } +// Spacer(modifier = Modifier.weight(1f)) + Column( + horizontalAlignment = Alignment.End + ) { + Row { + if (commentItem.unread) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(Color(0xFFE53935)) + .padding(4.dp) + ) { + Text( + text = "new", + color = Color.White, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.align(Alignment.Center) + ) + } + Spacer(modifier = Modifier.width(8.dp)) + } + Text( + text = commentItem.date.timeAgo(), + fontSize = 14.sp, + color = Color(0x66000000) + ) + } + Spacer(modifier = Modifier.height(4.dp)) + commentItem.post?.let { + CustomAsyncImage( + context = context, + imageUrl = it.images[0].thumbnail, + contentDescription = "Post Image", + modifier = Modifier + .size(32.dp) + .clip(RoundedCornerShape(4.dp)), + ) + } + } } + } } \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/message/MessageListViewModel.kt b/app/src/main/java/com/aiosman/riderpro/ui/message/MessageListViewModel.kt new file mode 100644 index 0000000..12ca610 --- /dev/null +++ b/app/src/main/java/com/aiosman/riderpro/ui/message/MessageListViewModel.kt @@ -0,0 +1,82 @@ +package com.aiosman.riderpro.ui.message + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn +import androidx.paging.map +import com.aiosman.riderpro.data.AccountNotice +import com.aiosman.riderpro.data.AccountService +import com.aiosman.riderpro.data.CommentEntity +import com.aiosman.riderpro.data.CommentPagingSource +import com.aiosman.riderpro.data.CommentRemoteDataSource +import com.aiosman.riderpro.data.CommentService +import com.aiosman.riderpro.data.TestAccountServiceImpl +import com.aiosman.riderpro.data.TestCommentServiceImpl +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +object MessageListViewModel : ViewModel() { + val accountService: AccountService = TestAccountServiceImpl() + var noticeInfo by mutableStateOf(null) + + private val commentService: CommentService = TestCommentServiceImpl() + private val _commentItemsFlow = MutableStateFlow>(PagingData.empty()) + val commentItemsFlow = _commentItemsFlow.asStateFlow() + + suspend fun initData() { + val info = accountService.getMyNoticeInfo() + noticeInfo = info + } + + val likeNoticeCount + get() = noticeInfo?.likeCount ?: 0 + val followNoticeCount + get() = noticeInfo?.followCount ?: 0 + val favouriteNoticeCount + get() = noticeInfo?.favoriteCount ?: 0 + val commentNoticeCount + get() = noticeInfo?.commentCount ?: 0 + + init { + viewModelScope.launch { + Pager( + config = PagingConfig(pageSize = 5, enablePlaceholders = false), + pagingSourceFactory = { + CommentPagingSource( + CommentRemoteDataSource(commentService), + selfNotice = true + ) + } + ).flow.cachedIn(viewModelScope).collectLatest { + _commentItemsFlow.value = it + } + } + } + + private fun updateIsRead(id: Int) { + val currentPagingData = _commentItemsFlow.value + val updatedPagingData = currentPagingData.map { commentEntity -> + if (commentEntity.id == id) { + commentEntity.copy(unread = false) + } else { + commentEntity + } + } + _commentItemsFlow.value = updatedPagingData + } + + fun updateReadStatus(id: Int) { + viewModelScope.launch { + commentService.updateReadStatus(id) + updateIsRead(id) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt b/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt index 642480f..19ca160 100644 --- a/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt +++ b/app/src/main/java/com/aiosman/riderpro/ui/post/Post.kt @@ -87,6 +87,7 @@ import com.aiosman.riderpro.data.TestAccountServiceImpl import com.aiosman.riderpro.data.TestMomentServiceImpl import com.aiosman.riderpro.data.TestUserServiceImpl import com.aiosman.riderpro.data.UserService +import com.aiosman.riderpro.exp.timeAgo import com.aiosman.riderpro.model.MomentEntity import com.aiosman.riderpro.model.MomentImageEntity import com.aiosman.riderpro.ui.NavigationRoute @@ -571,7 +572,7 @@ fun CommentItem(commentEntity: CommentEntity, onLike: () -> Unit = {}) { Column { Text(text = commentEntity.name, fontWeight = FontWeight.Bold) Text(text = commentEntity.comment) - Text(text = commentEntity.date, fontSize = 12.sp, color = Color.Gray) + Text(text = commentEntity.date.timeAgo(), fontSize = 12.sp, color = Color.Gray) } Spacer(modifier = Modifier.weight(1f)) Column(horizontalAlignment = Alignment.CenterHorizontally) { diff --git a/app/src/main/java/com/aiosman/riderpro/utils/Utils.kt b/app/src/main/java/com/aiosman/riderpro/utils/Utils.kt index 044cf89..e44cc90 100644 --- a/app/src/main/java/com/aiosman/riderpro/utils/Utils.kt +++ b/app/src/main/java/com/aiosman/riderpro/utils/Utils.kt @@ -4,6 +4,8 @@ import android.content.Context import coil.ImageLoader import coil.request.CachePolicy import com.aiosman.riderpro.data.api.getUnsafeOkHttpClient +import java.util.Date +import java.util.concurrent.TimeUnit object Utils { fun generateRandomString(length: Int): String { @@ -12,6 +14,7 @@ object Utils { .map { allowedChars.random() } .joinToString("") } + fun getImageLoader(context: Context): ImageLoader { val okHttpClient = getUnsafeOkHttpClient() return ImageLoader.Builder(context) @@ -21,7 +24,25 @@ object Utils { .components { - } - .build() + }.build() + } + + fun getTimeAgo(date: Date): String { + val now = Date() + val diffInMillis = now.time - date.time + + val seconds = TimeUnit.MILLISECONDS.toSeconds(diffInMillis) + val minutes = TimeUnit.MILLISECONDS.toMinutes(diffInMillis) + val hours = TimeUnit.MILLISECONDS.toHours(diffInMillis) + val days = TimeUnit.MILLISECONDS.toDays(diffInMillis) + val years = days / 365 + + return when { + seconds < 60 -> "$seconds seconds ago" + minutes < 60 -> "$minutes minutes ago" + hours < 24 -> "$hours hours ago" + days < 365 -> "$days days ago" + else -> "$years years ago" + } } } \ No newline at end of file