diff --git a/android/.idea/deploymentTargetDropDown.xml b/android/.idea/deploymentTargetDropDown.xml deleted file mode 100644 index 7e0dc85b..00000000 --- a/android/.idea/deploymentTargetDropDown.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/RootActivity.kt b/android/app/src/main/java/com/goliath/emojihub/RootActivity.kt index 6f700095..c956dd6e 100644 --- a/android/app/src/main/java/com/goliath/emojihub/RootActivity.kt +++ b/android/app/src/main/java/com/goliath/emojihub/RootActivity.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -32,8 +33,11 @@ import com.goliath.emojihub.views.LoginPage import com.goliath.emojihub.views.MainPage import com.goliath.emojihub.views.SignUpPage import com.goliath.emojihub.views.TransformVideoPage +import com.goliath.emojihub.views.components.CreatedEmojiListView +import com.goliath.emojihub.views.components.CreatedPostListView import com.goliath.emojihub.views.components.CustomDialog import com.goliath.emojihub.views.components.PlayEmojiView +import com.goliath.emojihub.views.components.SavedEmojiListView import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -50,7 +54,10 @@ class RootActivity : ComponentActivity() { setContent { EmojiHubTheme { - Box(Modifier.fillMaxSize().background(Color.White)) { + Box( + Modifier + .fillMaxSize() + .background(Color.White)) { val accessToken = userViewModel.accessTokenState.collectAsState().value val error by apiErrorController.apiErrorState.collectAsState() @@ -110,20 +117,62 @@ class RootActivity : ComponentActivity() { } composable(NavigationDestination.TransformVideo) { - val emojiViewModel = hiltViewModel() + val parentEntry = remember(it) { + navController.getBackStackEntry(NavigationDestination.MainPage) + } + val emojiViewModel = hiltViewModel(parentEntry) TransformVideoPage(emojiViewModel) } composable(NavigationDestination.PlayEmojiVideo) { - val emojiViewModel = hiltViewModel() - PlayEmojiView(emojiViewModel) + val parentEntry = remember(it) { + navController.getBackStackEntry(NavigationDestination.MainPage) + } + val emojiViewModel = hiltViewModel(parentEntry) + val userViewModel = hiltViewModel(parentEntry) + PlayEmojiView(emojiViewModel, userViewModel) } composable(NavigationDestination.CreatePost) { - val postViewModel = hiltViewModel() + val parentEntry = remember(it) { + navController.getBackStackEntry(NavigationDestination.MainPage) + } + val postViewModel = hiltViewModel(parentEntry) CreatePostPage(postViewModel) } + + composable(NavigationDestination.MyPostList) { + val parentEntry = remember(it) { + navController.getBackStackEntry(NavigationDestination.MainPage) + } + val postViewModel = hiltViewModel(parentEntry) + CreatedPostListView(postViewModel.myPostList) + } + + composable(NavigationDestination.MyEmojiList) { + val parentEntry = remember(it) { + navController.getBackStackEntry(NavigationDestination.MainPage) + } + val emojiViewModel = hiltViewModel(parentEntry) + CreatedEmojiListView(emojiViewModel) + } + + composable(NavigationDestination.MySavedEmojiList) { + val parentEntry = remember(it) { + navController.getBackStackEntry(NavigationDestination.MainPage) + } + val emojiViewModel = hiltViewModel(parentEntry) + SavedEmojiListView(emojiViewModel) + } } } } +} + +fun NavController.navigateAsOrigin(route: String) { + navigate(route) { + while (popBackStack()) { } + launchSingleTop = true + restoreState = true + } } \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/data_sources/ApiErrorController.kt b/android/app/src/main/java/com/goliath/emojihub/data_sources/ApiErrorController.kt index 9715a26b..69f5a820 100644 --- a/android/app/src/main/java/com/goliath/emojihub/data_sources/ApiErrorController.kt +++ b/android/app/src/main/java/com/goliath/emojihub/data_sources/ApiErrorController.kt @@ -51,7 +51,7 @@ enum class CustomError( override fun body(): String = "이미 있는 계정입니다." }, INTERNAL_SERVER_ERROR(500) { - override fun body(): String = "접속 오류가 발생했습니다." + override fun body(): String = "네트워크 접속 오류가 발생했습니다." },; companion object { diff --git a/android/app/src/main/java/com/goliath/emojihub/data_sources/BottomNavigationController.kt b/android/app/src/main/java/com/goliath/emojihub/data_sources/BottomNavigationController.kt index d8ea772f..77ec7b80 100644 --- a/android/app/src/main/java/com/goliath/emojihub/data_sources/BottomNavigationController.kt +++ b/android/app/src/main/java/com/goliath/emojihub/data_sources/BottomNavigationController.kt @@ -1,7 +1,7 @@ package com.goliath.emojihub.data_sources -import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable +import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import com.goliath.emojihub.views.PageItem @@ -9,7 +9,7 @@ class BottomNavigationController { private val _bottomNavigationDestination = mutableStateOf(PageItem.Feed.screenRoute) @Stable - val currentDestination: MutableState = _bottomNavigationDestination + val currentDestination: State = _bottomNavigationDestination fun updateDestination(destination: PageItem) { _bottomNavigationDestination.value = destination.screenRoute diff --git a/android/app/src/main/java/com/goliath/emojihub/data_sources/DataSourceModule.kt b/android/app/src/main/java/com/goliath/emojihub/data_sources/DataSourceModule.kt index 72ca0e40..01f122d7 100644 --- a/android/app/src/main/java/com/goliath/emojihub/data_sources/DataSourceModule.kt +++ b/android/app/src/main/java/com/goliath/emojihub/data_sources/DataSourceModule.kt @@ -2,6 +2,8 @@ package com.goliath.emojihub.data_sources import com.goliath.emojihub.data_sources.local.X3dDataSource import com.goliath.emojihub.data_sources.local.X3dDataSourceImpl +import com.goliath.emojihub.data_sources.remote.EmojiDataSource +import com.goliath.emojihub.data_sources.remote.EmojiDataSourceImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -13,6 +15,9 @@ abstract class DataSourceModule { @Binds abstract fun bindsX3dDataSource(impl: X3dDataSourceImpl): X3dDataSource + @Binds + abstract fun bindsEmojiDataSource(impl: EmojiDataSourceImpl): EmojiDataSource + @Binds abstract fun bindsApiErrorController(impl: ApiErrorControllerImpl): ApiErrorController } \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/data_sources/EmojiPagingSource.kt b/android/app/src/main/java/com/goliath/emojihub/data_sources/EmojiPagingSource.kt index 8774e6bc..5b60c5db 100644 --- a/android/app/src/main/java/com/goliath/emojihub/data_sources/EmojiPagingSource.kt +++ b/android/app/src/main/java/com/goliath/emojihub/data_sources/EmojiPagingSource.kt @@ -12,6 +12,7 @@ enum class EmojiFetchType { class EmojiPagingSource @Inject constructor( private val api: EmojiApi, + private val sortByDate : Int, private val type: EmojiFetchType ): PagingSource() { override suspend fun load(params: LoadParams): LoadResult { @@ -20,20 +21,21 @@ class EmojiPagingSource @Inject constructor( return try { val response: List? = when (type) { EmojiFetchType.GENERAL -> { - api.fetchEmojiList(1, cursor, count).body() + api.fetchEmojiList(sortByDate, cursor, count).body() } EmojiFetchType.MY_CREATED -> { - api.fetchMyCreatedEmojiList(1, cursor, count).body() + api.fetchMyCreatedEmojiList(sortByDate, cursor, count).body() } EmojiFetchType.MY_SAVED -> { - api.fetchMySavedEmojiList(1, cursor, count).body() + api.fetchMySavedEmojiList(sortByDate, cursor, count).body() } } val data = response ?: listOf() + val nextKey = if (response?.size!! < count) null else cursor + 1 //Stops infinite fetching LoadResult.Page( data = data, prevKey = if (cursor == 1) null else cursor - 1, - nextKey = if (data.isEmpty()) null else cursor + 1 + nextKey = nextKey ) } catch (exception: Exception) { LoadResult.Error(exception) diff --git a/android/app/src/main/java/com/goliath/emojihub/data_sources/NetworkModule.kt b/android/app/src/main/java/com/goliath/emojihub/data_sources/NetworkModule.kt index 7c42d631..4851aa09 100644 --- a/android/app/src/main/java/com/goliath/emojihub/data_sources/NetworkModule.kt +++ b/android/app/src/main/java/com/goliath/emojihub/data_sources/NetworkModule.kt @@ -3,6 +3,7 @@ package com.goliath.emojihub.data_sources import com.goliath.emojihub.EmojiHubApplication import com.goliath.emojihub.data_sources.api.EmojiApi import com.goliath.emojihub.data_sources.api.PostApi +import com.goliath.emojihub.data_sources.api.ReactionApi import com.goliath.emojihub.data_sources.api.UserApi import dagger.Module import dagger.Provides @@ -59,6 +60,11 @@ object NetworkModule { fun providesPostRestApi(retrofit: Retrofit): PostApi = retrofit.create(PostApi::class.java) + @Provides + @Singleton + fun providesReactionRestApi(retrofit: Retrofit): ReactionApi = + retrofit.create(ReactionApi::class.java) + // empty responses should be handled `success` private val nullOnEmptyConverterFactory = object : Converter.Factory() { fun converterFactory() = this diff --git a/android/app/src/main/java/com/goliath/emojihub/data_sources/ReactionPagingSource.kt b/android/app/src/main/java/com/goliath/emojihub/data_sources/ReactionPagingSource.kt new file mode 100644 index 00000000..6a4992f1 --- /dev/null +++ b/android/app/src/main/java/com/goliath/emojihub/data_sources/ReactionPagingSource.kt @@ -0,0 +1,37 @@ +package com.goliath.emojihub.data_sources + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.goliath.emojihub.data_sources.api.ReactionApi +import com.goliath.emojihub.models.ReactionWithEmojiDto +import javax.inject.Inject + +class ReactionPagingSource @Inject constructor( + private val api: ReactionApi, + private val postId: String, + private val emojiUnicode: String +): PagingSource(){ + override suspend fun load(params: LoadParams): LoadResult { + val cursor = params.key ?: 1 + val count = params.loadSize + return try { + val response: List? = api.fetchReactionList(postId, emojiUnicode, cursor, count).body() + val data = response ?: listOf() + val nextKey = if (response?.size!! < count) null else cursor + 1 //Stops infinite fetching + LoadResult.Page( + data = data, + prevKey = if (cursor == 1) null else cursor - 1, + nextKey = nextKey + ) + } catch (exception: Exception) { + LoadResult.Error(exception) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) + } + } +} diff --git a/android/app/src/main/java/com/goliath/emojihub/data_sources/SharedLocalStorage.kt b/android/app/src/main/java/com/goliath/emojihub/data_sources/SharedLocalStorage.kt index 7f064e6e..70539e9e 100644 --- a/android/app/src/main/java/com/goliath/emojihub/data_sources/SharedLocalStorage.kt +++ b/android/app/src/main/java/com/goliath/emojihub/data_sources/SharedLocalStorage.kt @@ -8,7 +8,7 @@ import javax.inject.Singleton interface LocalStorage { var accessToken: String? - val currentUser: String? + var currentUser: String? } @Singleton diff --git a/android/app/src/main/java/com/goliath/emojihub/data_sources/api/ReactionApi.kt b/android/app/src/main/java/com/goliath/emojihub/data_sources/api/ReactionApi.kt new file mode 100644 index 00000000..8c8f5459 --- /dev/null +++ b/android/app/src/main/java/com/goliath/emojihub/data_sources/api/ReactionApi.kt @@ -0,0 +1,35 @@ +package com.goliath.emojihub.data_sources.api + +import com.goliath.emojihub.models.ReactionWithEmojiDto +import retrofit2.Response +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query + +interface ReactionApi { + @POST("reaction") + suspend fun uploadReaction( + @Query("postId") postId: String, + @Query("emojiId") emojiId: String + ): Response + + @GET("reaction") + suspend fun fetchReactionList( + @Query("postId") postId: String, + @Query("emojiUnicode") emojiUnicode: String, + @Query("index") index: Int, + @Query("count") count: Int + ): Response> + + @GET("reaction") + suspend fun getReactionWithId( + @Path("id") id: String + ): Response + + @DELETE("reaction") + suspend fun deleteReaction( + @Query("reactionId") reactionId: String + ): Response +} \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/data_sources/api/UserApi.kt b/android/app/src/main/java/com/goliath/emojihub/data_sources/api/UserApi.kt index 9721787b..170949ef 100644 --- a/android/app/src/main/java/com/goliath/emojihub/data_sources/api/UserApi.kt +++ b/android/app/src/main/java/com/goliath/emojihub/data_sources/api/UserApi.kt @@ -2,7 +2,7 @@ package com.goliath.emojihub.data_sources.api import com.goliath.emojihub.models.LoginUserDto import com.goliath.emojihub.models.RegisterUserDto -import com.goliath.emojihub.models.UserDtoList +import com.goliath.emojihub.models.UserDetailsDto import com.goliath.emojihub.models.responses.LoginResponseDto import retrofit2.Response import retrofit2.http.Body @@ -15,7 +15,12 @@ interface UserApi { @GET("user") suspend fun fetchUserList( - ): Response> + ): Response> + + @GET("user/me") + suspend fun fetchMyInfo( + @Header("Authorization") authToken: String + ): Response @POST("user/signup") suspend fun registerUser( diff --git a/android/app/src/main/java/com/goliath/emojihub/data_sources/remote/EmojiDataSource.kt b/android/app/src/main/java/com/goliath/emojihub/data_sources/remote/EmojiDataSource.kt new file mode 100644 index 00000000..a7bf5e21 --- /dev/null +++ b/android/app/src/main/java/com/goliath/emojihub/data_sources/remote/EmojiDataSource.kt @@ -0,0 +1,40 @@ +package com.goliath.emojihub.data_sources.remote + +import android.content.Context +import android.graphics.Bitmap +import android.media.MediaMetadataRetriever +import android.util.Log +import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.File +import java.io.FileOutputStream +import javax.inject.Inject + +interface EmojiDataSource { + fun createVideoThumbNail(videoFile: File): File? +} +class EmojiDataSourceImpl @Inject constructor( + @ApplicationContext private val context: Context +): EmojiDataSource { + + override fun createVideoThumbNail(videoFile: File): File? { + val retriever = MediaMetadataRetriever() + try { + retriever.setDataSource(videoFile.absolutePath) + val bitmap = retriever.frameAtTime + + bitmap?.let { + val thumbnailFile = File(context.cacheDir, "thumbnail_${videoFile.name}.jpg") + FileOutputStream(thumbnailFile).use { out -> + bitmap.compress(Bitmap.CompressFormat.JPEG, 75, out) + } + Log.d("EmojiDataSource", "Thumbnail created: ${thumbnailFile.absolutePath}") + return thumbnailFile + } + } catch (e: Exception) { + Log.e("EmojiDataSource", "ERROR creating thumbnail: ${e.message?:"Unknown error"}") + } finally { + retriever.release() + } + return null + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/extensions/Extensions.kt b/android/app/src/main/java/com/goliath/emojihub/extensions/Extensions.kt index 417c5724..5c006bbb 100644 --- a/android/app/src/main/java/com/goliath/emojihub/extensions/Extensions.kt +++ b/android/app/src/main/java/com/goliath/emojihub/extensions/Extensions.kt @@ -1,5 +1,7 @@ package com.goliath.emojihub.extensions +import com.goliath.emojihub.models.ReactionWithEmojiUnicode + fun String.toEmoji(): String { return try { this.trim().split(" ").map { it.removePrefix("U+").toInt(16) } @@ -9,18 +11,20 @@ fun String.toEmoji(): String { } } -fun reactionsToString (reactions: List): String { +fun reactionsToString (reactions: List): String { var emojisStr = "" if (reactions.size >= 3) { - for (emoji in reactions) { - emojisStr += emoji + val lastThreeReactions = reactions.takeLast(3) + for (reaction in lastThreeReactions) { + emojisStr += reaction.emoji_unicode.toEmoji() emojisStr += " " } - emojisStr += "외 ${reactions.size - 3}개의 반응" + emojisStr += if (reactions.size == 3) "3개의 반응" + else "외 ${reactions.size - 3}개의 반응" } else { - for (emoji in reactions) { - emojisStr += emoji + for (reaction in reactions) { + emojisStr += reaction.emoji_unicode.toEmoji() emojisStr += " " } emojisStr += "${reactions.size}개의 반응" diff --git a/android/app/src/main/java/com/goliath/emojihub/models/Post.kt b/android/app/src/main/java/com/goliath/emojihub/models/Post.kt index 58252c7c..cae22752 100644 --- a/android/app/src/main/java/com/goliath/emojihub/models/Post.kt +++ b/android/app/src/main/java/com/goliath/emojihub/models/Post.kt @@ -10,7 +10,7 @@ class Post( val modifiedAt: String = dto.modifiedAt val createdBy: String = dto.createdBy val content: String = dto.content - val reaction: List = dto.reaction + val reaction: List = dto.reaction override fun equals(other: Any?): Boolean { if (this === other) return true @@ -44,22 +44,14 @@ data class PostDto( @SerializedName("modified_at") val modifiedAt: String, @SerializedName("created_by") val createdBy: String, val content: String, - @SerializedName("reactions") val reaction: List + @SerializedName("reactions") val reaction: List ) data class UploadPostDto( @SerializedName("content") val content: String ) -val dummyPost = Post( - PostDto( - id = "1234", - createdAt = "2023.09.16", - createdBy = "channn", - content = "조금 전에 앞에 계신 분이 실수로 지갑을 흘리셨다. " + - "지갑이 하수구 구멍으로 빠지려는 찰나, 발로 굴러가는 지갑을 막아서 다행히 참사는 막을 수 있었다. " + - "지갑 주인분께서 감사하다고 카페 드림에서 커피도 한 잔 사주셨다.", - modifiedAt = "2023.10.23", - reaction = listOf("good", "check", "good") - ) +data class ReactionWithEmojiUnicode( + var id: String = "", + var emoji_unicode: String = "" ) \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/models/Reaction.kt b/android/app/src/main/java/com/goliath/emojihub/models/Reaction.kt index f72648d3..109e0c08 100644 --- a/android/app/src/main/java/com/goliath/emojihub/models/Reaction.kt +++ b/android/app/src/main/java/com/goliath/emojihub/models/Reaction.kt @@ -2,12 +2,49 @@ package com.goliath.emojihub.models import com.google.gson.annotations.SerializedName +class Reaction( + dto: ReactionDto +) { + val id: String = dto.id + val createdAt: String = dto.createdAt + val createdBy: String = dto.createdBy + val emojiId: String = dto.emojiId + val postId: String = dto.postId +} data class ReactionMetaDataDto( @SerializedName("emoji_unicode_list") val unicodeList: List ) data class ReactionDto( - @SerializedName("emoji_list") - val emojiList: List -) \ No newline at end of file + val id: String, + @SerializedName("created_at") val createdAt: String, + @SerializedName("created_by") val createdBy: String, + @SerializedName("emoji_id") val emojiId: String, + @SerializedName("post_id") val postId: String +) + +data class UploadReactionDto( + @SerializedName("postId") val postId: String, + @SerializedName("emojiId") val emojiId: String +) + +data class ReactionWithEmojiDto( + val id: String, + @SerializedName("created_at") val createdAt: String, + @SerializedName("created_by") val createdBy: String, + @SerializedName("emoji_id") val emojiId: String, + @SerializedName("post_id") val postId: String, + @SerializedName("emojiDto") val emojiDto: EmojiDto? +) + +class ReactionWithEmoji( + dto: ReactionWithEmojiDto +) { + val id: String = dto.id + val createdAt: String = dto.createdAt + val createdBy: String = dto.createdBy + val emojiId: String = dto.emojiId + val postId: String = dto.postId + val emojiDto: EmojiDto? = dto.emojiDto +} diff --git a/android/app/src/main/java/com/goliath/emojihub/models/User.kt b/android/app/src/main/java/com/goliath/emojihub/models/User.kt index e4405115..3253a32d 100644 --- a/android/app/src/main/java/com/goliath/emojihub/models/User.kt +++ b/android/app/src/main/java/com/goliath/emojihub/models/User.kt @@ -12,8 +12,41 @@ data class UserDto( val name: String ) -// user list: will be deprecated -data class UserDtoList( +class UserDetails( + dto: UserDetailsDto +) { + val name: String = dto.name + val email: String = dto.email + val savedEmojiList: List? = dto.savedEmojiList + val createdEmojiList: List? = dto.createdEmojiList + val createdPostList: List? = dto.createdPostList + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UserDetails + + if (name != other.name) return false + if (email != other.email) return false + if (savedEmojiList != other.savedEmojiList) return false + if (createdEmojiList != other.createdEmojiList) return false + if (createdPostList != other.createdPostList) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + email.hashCode() + result = 31 * result + (savedEmojiList?.hashCode() ?: 0) + result = 31 * result + (createdEmojiList?.hashCode() ?: 0) + result = 31 * result + (createdPostList?.hashCode() ?: 0) + return result + } +} + +data class UserDetailsDto( @SerializedName("email") val email: String, @@ -23,11 +56,14 @@ data class UserDtoList( @SerializedName("password") val password: String, - @SerializedName("liked_emojis") - val likedEmojiList: String?, + @SerializedName("saved_emojis") + val savedEmojiList: List?, @SerializedName("created_emojis") - val createdEmojiList: String? + val createdEmojiList: List?, + + @SerializedName("created_posts") + val createdPostList: List? ) class RegisterUserDto( diff --git a/android/app/src/main/java/com/goliath/emojihub/repositories/RepositoryModule.kt b/android/app/src/main/java/com/goliath/emojihub/repositories/RepositoryModule.kt index d6a431e0..8aea92bc 100644 --- a/android/app/src/main/java/com/goliath/emojihub/repositories/RepositoryModule.kt +++ b/android/app/src/main/java/com/goliath/emojihub/repositories/RepositoryModule.kt @@ -6,6 +6,8 @@ import com.goliath.emojihub.repositories.remote.EmojiRepository import com.goliath.emojihub.repositories.remote.EmojiRepositoryImpl import com.goliath.emojihub.repositories.remote.PostRepository import com.goliath.emojihub.repositories.remote.PostRepositoryImpl +import com.goliath.emojihub.repositories.remote.ReactionRepository +import com.goliath.emojihub.repositories.remote.ReactionRepositoryImpl import com.goliath.emojihub.repositories.remote.UserRepository import com.goliath.emojihub.repositories.remote.UserRepositoryImpl import dagger.Binds @@ -27,4 +29,7 @@ abstract class RepositoryModule { @Binds abstract fun bindsX3dRepository(impl: X3dRepositoryImpl): X3dRepository + + @Binds + abstract fun bindsReactionRepository(impl: ReactionRepositoryImpl): ReactionRepository } \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/repositories/local/X3dRepository.kt b/android/app/src/main/java/com/goliath/emojihub/repositories/local/X3dRepository.kt index 469949c4..2d41f462 100644 --- a/android/app/src/main/java/com/goliath/emojihub/repositories/local/X3dRepository.kt +++ b/android/app/src/main/java/com/goliath/emojihub/repositories/local/X3dRepository.kt @@ -10,6 +10,7 @@ import javax.inject.Inject import javax.inject.Singleton interface X3dRepository { + val DEFAULT_EMOJI_LIST: List suspend fun createEmoji(videoUri: Uri, topK: Int): List } @@ -17,19 +18,20 @@ interface X3dRepository { class X3dRepositoryImpl @Inject constructor( private val x3dDataSource: X3dDataSource ): X3dRepository { + + // FIXME: Default emojis should be topK different emojis -> use just 3 emojis for now + override val DEFAULT_EMOJI_LIST = listOf( + CreatedEmoji("love it", "U+2764 U+FE0F"), + CreatedEmoji("like", "U+1F44D"), + CreatedEmoji("ok", "U+1F646") + ) companion object{ const val moduleName = "Hagrid/efficient_x3d_s_hagrid_float.pt" const val idToClassFileName = "Hagrid/hagrid_id_to_classname.json" const val classToUnicodeFileName = "Hagrid/hagrid_classname_to_unicode.json" const val SCORE_THRESHOLD = 0.4F - // FIXME: Default emojis should be topK different emojis - const val DEFAULT_EMOJI_NAME_1 = "love it" - const val DEFAULT_EMOJI_UNICODE_1 = "U+2764 U+FE0F" - const val DEFAULT_EMOJI_NAME_2 = "like" - const val DEFAULT_EMOJI_UNICODE_2 = "U+1F44D" - const val DEFAULT_EMOJI_NAME_3 = "ok" - const val DEFAULT_EMOJI_UNICODE_3 = "U+1F646" } + override suspend fun createEmoji(videoUri: Uri, topK: Int): List { val x3dModule = x3dDataSource.loadModule(moduleName) ?: return emptyList() @@ -57,9 +59,7 @@ class X3dRepositoryImpl @Inject constructor( val inferenceResults = x3dDataSource.runInference(x3dModule, videoTensor, topK) if (inferenceResults.isEmpty() || inferenceResults[0].score < SCORE_THRESHOLD) { Log.w("X3d Repository", "Score is lower than threshold, return default emoji") - return listOf(CreatedEmoji(DEFAULT_EMOJI_NAME_1, DEFAULT_EMOJI_UNICODE_1), - CreatedEmoji(DEFAULT_EMOJI_NAME_2, DEFAULT_EMOJI_UNICODE_2), - CreatedEmoji(DEFAULT_EMOJI_NAME_3, DEFAULT_EMOJI_UNICODE_3)) + return DEFAULT_EMOJI_LIST } return x3dDataSource.indexToCreatedEmojiList( inferenceResults, idToClassFileName, classToUnicodeFileName diff --git a/android/app/src/main/java/com/goliath/emojihub/repositories/remote/EmojiRepository.kt b/android/app/src/main/java/com/goliath/emojihub/repositories/remote/EmojiRepository.kt index 8c78d3d5..0b01cec6 100644 --- a/android/app/src/main/java/com/goliath/emojihub/repositories/remote/EmojiRepository.kt +++ b/android/app/src/main/java/com/goliath/emojihub/repositories/remote/EmojiRepository.kt @@ -1,66 +1,59 @@ package com.goliath.emojihub.repositories.remote -import android.content.Context -import android.graphics.Bitmap -import android.media.MediaMetadataRetriever -import android.util.Log import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData import com.goliath.emojihub.data_sources.EmojiFetchType import com.goliath.emojihub.data_sources.EmojiPagingSource import com.goliath.emojihub.data_sources.api.EmojiApi +import com.goliath.emojihub.data_sources.remote.EmojiDataSource import com.goliath.emojihub.models.EmojiDto import com.goliath.emojihub.models.UploadEmojiDto import com.google.gson.Gson -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.Flow import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody -import retrofit2.HttpException import retrofit2.Response import java.io.File -import java.io.FileOutputStream -import java.io.IOException import javax.inject.Inject import javax.inject.Singleton interface EmojiRepository { - suspend fun fetchEmojiList(): Flow> + suspend fun fetchEmojiList(sortByDate: Int): Flow> suspend fun fetchMyCreatedEmojiList(): Flow> suspend fun fetchMySavedEmojiList(): Flow> suspend fun getEmojiWithId(id: String): EmojiDto? - suspend fun uploadEmoji(videoFile: File, emojiDto: UploadEmojiDto): Boolean - suspend fun saveEmoji(id: String): Result - suspend fun unSaveEmoji(id: String): Result + suspend fun uploadEmoji(videoFile: File, emojiDto: UploadEmojiDto): Response + suspend fun saveEmoji(id: String): Response + suspend fun unSaveEmoji(id: String): Response suspend fun deleteEmoji(id: String): Response } @Singleton class EmojiRepositoryImpl @Inject constructor( private val emojiApi: EmojiApi, - @ApplicationContext private val context: Context + private val emojiDataSource: EmojiDataSource ): EmojiRepository { - override suspend fun fetchEmojiList(): Flow> { + override suspend fun fetchEmojiList(sortByDate: Int): Flow> { return Pager( config = PagingConfig(pageSize = 10, initialLoadSize = 10, enablePlaceholders = false), - pagingSourceFactory = { EmojiPagingSource(emojiApi, EmojiFetchType.GENERAL) } + pagingSourceFactory = { EmojiPagingSource(emojiApi, sortByDate, EmojiFetchType.GENERAL) } ).flow } override suspend fun fetchMyCreatedEmojiList(): Flow> { return Pager( config = PagingConfig(pageSize = 10, initialLoadSize = 10, enablePlaceholders = false), - pagingSourceFactory = { EmojiPagingSource(emojiApi, EmojiFetchType.MY_CREATED) } + pagingSourceFactory = { EmojiPagingSource(emojiApi, 1, EmojiFetchType.MY_CREATED) } ).flow } override suspend fun fetchMySavedEmojiList(): Flow> { return Pager( config = PagingConfig(pageSize = 10, initialLoadSize = 10, enablePlaceholders = false), - pagingSourceFactory = { EmojiPagingSource(emojiApi, EmojiFetchType.MY_SAVED) } + pagingSourceFactory = { EmojiPagingSource(emojiApi, 1, EmojiFetchType.MY_SAVED) } ).flow } @@ -68,93 +61,31 @@ class EmojiRepositoryImpl @Inject constructor( TODO("Not yet implemented") } - override suspend fun uploadEmoji(videoFile: File, emojiDto: UploadEmojiDto): Boolean { + override suspend fun uploadEmoji(videoFile: File, emojiDto: UploadEmojiDto): Response { val emojiDtoJson = Gson().toJson(emojiDto) val emojiDtoRequestBody = emojiDtoJson.toRequestBody("application/json".toMediaTypeOrNull()) val videoFileRequestBody = videoFile.asRequestBody("video/mp4".toMediaTypeOrNull()) val videoFileMultipartBody = MultipartBody.Part.createFormData("file", videoFile.name, videoFileRequestBody) - val thumbnailFile = createVideoThumbnail(context, videoFile) + val thumbnailFile = emojiDataSource.createVideoThumbNail(videoFile) val thumbnailRequestBody = thumbnailFile!! .asRequestBody("image/jpg".toMediaTypeOrNull()) val thumbnailMultipartBody = MultipartBody.Part.createFormData("thumbnail", thumbnailFile.name, thumbnailRequestBody) - return try { - emojiApi.uploadEmoji(videoFileMultipartBody, thumbnailMultipartBody, emojiDtoRequestBody) - true - } - catch (e: IOException) { - Log.e("EmojiRepository", "IOException") - false - } - catch (e: HttpException) { - Log.e("EmojiRepository", "HttpException") - false - } - catch (e: Exception) { - Log.e("EmojiRepository", "Unknown Exception: ${e.message}") - false - } + return emojiApi.uploadEmoji(videoFileMultipartBody, thumbnailMultipartBody, emojiDtoRequestBody) } - override suspend fun saveEmoji(id: String): Result { - return try { - val response = emojiApi.saveEmoji(id) - Log.d("EmojiRepository", "SaveEmoji Api response : ${response.code()}") - - if (response.isSuccessful) { - Log.d("EmojiRepository", "Successfully saved Emoji (Id: $id)") - Result.success(Unit) - } else { - Log.d("EmojiRepository", "Failed to save Emoji (Id: $id), ${response.code()}") - Result.failure(Exception("Failed to save Emoji (Id: $id), ${response.code()}")) - } - } catch (e: Exception) { - Result.failure(e) - } + override suspend fun saveEmoji(id: String): Response { + return emojiApi.saveEmoji(id) } - override suspend fun unSaveEmoji(id: String): Result { - return try { - val response = emojiApi.unSaveEmoji(id) - Log.d("EmojiRepository", "UnSaveEmoji Api response : ${response.code()}") - if (response.isSuccessful) { - Log.d("EmojiRepository", "Successfully unsaved Emoji (Id: $id)") - Result.success(Unit) - } else { - Log.d("EmojiRepository", "Failed to unsave Emoji (Id: $id), ${response.code()}") - Result.failure(Exception("Failed to unsave Emoji (Id: $id), ${response.code()}")) - } - } catch (e: Exception) { - Result.failure(e) - } + override suspend fun unSaveEmoji(id: String): Response { + return emojiApi.unSaveEmoji(id) } override suspend fun deleteEmoji(id: String): Response { TODO("Not yet implemented") } - - fun createVideoThumbnail(context: Context, videoFile: File): File? { - val retriever = MediaMetadataRetriever() - try { - retriever.setDataSource(videoFile.absolutePath) - val bitmap = retriever.frameAtTime - - bitmap?.let { - val thumbnailFile = File(context.cacheDir, "thumbnail_${videoFile.name}.jpg") - FileOutputStream(thumbnailFile).use { out -> - bitmap.compress(Bitmap.CompressFormat.JPEG, 75, out) - } - Log.d("create_TN", "Thumbnail created: ${thumbnailFile.absolutePath}") - return thumbnailFile - } - } catch (e: Exception) { - Log.e("EmojiRepository_create_TN", "ERROR creating thumbnail: ${e.message?:"Unknown error"}") - } finally { - retriever.release() - } - return null - } } \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/repositories/remote/ReactionRepository.kt b/android/app/src/main/java/com/goliath/emojihub/repositories/remote/ReactionRepository.kt new file mode 100644 index 00000000..9e8ba76c --- /dev/null +++ b/android/app/src/main/java/com/goliath/emojihub/repositories/remote/ReactionRepository.kt @@ -0,0 +1,43 @@ +package com.goliath.emojihub.repositories.remote + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.goliath.emojihub.data_sources.ReactionPagingSource +import com.goliath.emojihub.data_sources.api.ReactionApi +import com.goliath.emojihub.models.ReactionWithEmojiDto +import kotlinx.coroutines.flow.Flow +import retrofit2.Response +import javax.inject.Inject +import javax.inject.Singleton + +interface ReactionRepository { + suspend fun fetchReactionList(postId: String, emojiUnicode: String): Flow> + suspend fun uploadReaction(postId: String, emojiId: String): Response + suspend fun getReactionWithId(id: String) + suspend fun deleteReaction(reactionId: String) +} + +@Singleton +class ReactionRepositoryImpl @Inject constructor( + private val reactionApi: ReactionApi +): ReactionRepository { + override suspend fun fetchReactionList(postId: String, emojiUnicode: String): Flow> { + return Pager( + config = PagingConfig(pageSize = 10, initialLoadSize = 10, enablePlaceholders = false), + pagingSourceFactory = { ReactionPagingSource(reactionApi, postId, emojiUnicode) } + ).flow + } + + override suspend fun uploadReaction(postId: String, emojiId: String): Response { + return reactionApi.uploadReaction(postId, emojiId) + } + + override suspend fun getReactionWithId(id: String) { + TODO() + } + + override suspend fun deleteReaction(reactionId: String) { + reactionApi.deleteReaction(reactionId) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/repositories/remote/UserRepository.kt b/android/app/src/main/java/com/goliath/emojihub/repositories/remote/UserRepository.kt index b330d564..bb9b0b84 100644 --- a/android/app/src/main/java/com/goliath/emojihub/repositories/remote/UserRepository.kt +++ b/android/app/src/main/java/com/goliath/emojihub/repositories/remote/UserRepository.kt @@ -3,15 +3,16 @@ package com.goliath.emojihub.repositories.remote import com.goliath.emojihub.data_sources.api.UserApi import com.goliath.emojihub.models.LoginUserDto import com.goliath.emojihub.models.RegisterUserDto -import com.goliath.emojihub.models.UserDtoList +import com.goliath.emojihub.models.UserDetailsDto import com.goliath.emojihub.models.responses.LoginResponseDto import retrofit2.Response import javax.inject.Inject import javax.inject.Singleton interface UserRepository { - suspend fun fetchUserList(): Array - fun fetchUser(id: String) + suspend fun fetchUserList(): Array + suspend fun fetchUser(id: String) + suspend fun fetchMyInfo(authToken: String): Response suspend fun registerUser(dto: RegisterUserDto): Response suspend fun login(dto: LoginUserDto): Response suspend fun logout(): Response @@ -22,14 +23,18 @@ interface UserRepository { class UserRepositoryImpl @Inject constructor( private val userApi: UserApi ): UserRepository { - override suspend fun fetchUserList(): Array { + override suspend fun fetchUserList(): Array { return userApi.fetchUserList().body() ?: arrayOf() } - override fun fetchUser(id: String) { + override suspend fun fetchUser(id: String) { TODO("Not yet implemented") } + override suspend fun fetchMyInfo(authToken: String): Response { + return userApi.fetchMyInfo(authToken) + } + override suspend fun registerUser(dto: RegisterUserDto): Response { return userApi.registerUser(dto) } diff --git a/android/app/src/main/java/com/goliath/emojihub/usecases/EmojiUseCase.kt b/android/app/src/main/java/com/goliath/emojihub/usecases/EmojiUseCase.kt index fa76ea58..4a3b1446 100644 --- a/android/app/src/main/java/com/goliath/emojihub/usecases/EmojiUseCase.kt +++ b/android/app/src/main/java/com/goliath/emojihub/usecases/EmojiUseCase.kt @@ -5,6 +5,7 @@ import android.util.Log import androidx.paging.PagingData import androidx.paging.map import com.goliath.emojihub.data_sources.ApiErrorController +import com.goliath.emojihub.data_sources.CustomError import com.goliath.emojihub.models.CreatedEmoji import com.goliath.emojihub.models.Emoji import com.goliath.emojihub.models.UploadEmojiDto @@ -13,8 +14,11 @@ import com.goliath.emojihub.repositories.remote.EmojiRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import java.io.File +import java.io.IOException +import java.net.ConnectException import javax.inject.Inject import javax.inject.Singleton @@ -25,13 +29,13 @@ interface EmojiUseCase { suspend fun updateEmojiList(data: PagingData) suspend fun updateMyCreatedEmojiList(data: PagingData) suspend fun updateMySavedEmojiList(data: PagingData) - suspend fun fetchEmojiList(): Flow> + suspend fun fetchEmojiList(sortByDate: Int): Flow> suspend fun fetchMyCreatedEmojiList(): Flow> suspend fun fetchMySavedEmojiList(): Flow> suspend fun createEmoji(videoUri: Uri, topK: Int): List suspend fun uploadEmoji(emojiUnicode: String, emojiLabel: String, videoFile: File): Boolean - suspend fun saveEmoji(id: String): Result - suspend fun unSaveEmoji(id: String): Result + suspend fun saveEmoji(id: String): Boolean + suspend fun unSaveEmoji(id: String): Boolean } @Singleton @@ -65,32 +69,94 @@ class EmojiUseCaseImpl @Inject constructor( _mySavedEmojiList.emit(data) } - override suspend fun fetchEmojiList(): Flow> { - return emojiRepository.fetchEmojiList().map { it.map { dto -> Emoji(dto) } } + override suspend fun fetchEmojiList(sortByDate: Int): Flow> { + return try { + emojiRepository.fetchEmojiList(sortByDate).map { it.map { dto -> Emoji(dto) } } + } catch (e: ConnectException) { + errorController.setErrorState(CustomError.INTERNAL_SERVER_ERROR.statusCode) + flowOf(PagingData.empty()) + } catch (e: Exception) { + Log.e("EmojiUseCase", "Unknown Exception on fetchMyEmojiList: ${e.message}") + flowOf(PagingData.empty()) + } } override suspend fun fetchMyCreatedEmojiList(): Flow> { - return emojiRepository.fetchMyCreatedEmojiList().map { it.map { dto -> Emoji(dto) } } + return try { + emojiRepository.fetchMyCreatedEmojiList().map { it.map { dto -> Emoji(dto) } } + } catch (e: ConnectException) { + errorController.setErrorState(CustomError.INTERNAL_SERVER_ERROR.statusCode) + flowOf(PagingData.empty()) + } catch (e: Exception) { + Log.e("EmojiUseCase", "Unknown Exception on fetchMyCreatedEmojiList: ${e.message}") + flowOf(PagingData.empty()) + } } override suspend fun fetchMySavedEmojiList(): Flow> { - return emojiRepository.fetchMySavedEmojiList().map { it.map { dto -> Emoji(dto) } } + return try { + emojiRepository.fetchMySavedEmojiList().map { it.map { dto -> Emoji(dto) } } + } catch (e: ConnectException) { + errorController.setErrorState(CustomError.INTERNAL_SERVER_ERROR.statusCode) + flowOf(PagingData.empty()) + } catch (e: Exception) { + Log.e("EmojiUseCase", "Unknown Exception on fetchMySavedEmojiList: ${e.message}") + flowOf(PagingData.empty()) + } } override suspend fun createEmoji(videoUri: Uri, topK: Int): List { - return x3dRepository.createEmoji(videoUri, topK) + return try { + x3dRepository.createEmoji(videoUri, topK) + } catch (e: Exception) { + Log.e("EmojiUseCase", "Unknown Exception on createEmoji: ${e.message}") + x3dRepository.DEFAULT_EMOJI_LIST + } } override suspend fun uploadEmoji(emojiUnicode: String, emojiLabel: String, videoFile: File): Boolean { val dto = UploadEmojiDto(emojiUnicode, emojiLabel) - return emojiRepository.uploadEmoji(videoFile, dto) + return try { + val response = emojiRepository.uploadEmoji(videoFile, dto) + if (response.isSuccessful) { + true + } else { + errorController.setErrorState(response.code()) + false + } + } catch (e: IOException) { + Log.e("EmojiUseCase", "IOException") + false + } catch (e: ConnectException) { + errorController.setErrorState(CustomError.INTERNAL_SERVER_ERROR.statusCode) + false + } catch (e: Exception) { + Log.e("EmojiUseCase", "Unknown Exception on uploadEmoji: ${e.message}") + false + } } - override suspend fun saveEmoji(id: String): Result { - return emojiRepository.saveEmoji(id) + override suspend fun saveEmoji(id: String): Boolean { + return try { + emojiRepository.saveEmoji(id).isSuccessful + } catch (e: ConnectException) { + errorController.setErrorState(CustomError.INTERNAL_SERVER_ERROR.statusCode) + false + } catch (e: Exception) { + Log.e("EmojiUseCase", "Unknown Exception on saveEmoji: ${e.message}") + false + } } - override suspend fun unSaveEmoji(id: String): Result { - return emojiRepository.unSaveEmoji(id) + override suspend fun unSaveEmoji(id: String): Boolean { + return try { + emojiRepository.unSaveEmoji(id).isSuccessful + } catch (e: ConnectException) { + errorController.setErrorState(CustomError.INTERNAL_SERVER_ERROR.statusCode) + false + } catch (e: Exception) { + Log.e("EmojiUseCase", "Unknown Exception on unSaveEmoji: ${e.message}") + false + } } } \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/usecases/PostUseCase.kt b/android/app/src/main/java/com/goliath/emojihub/usecases/PostUseCase.kt index 2ca9f337..cf007c78 100644 --- a/android/app/src/main/java/com/goliath/emojihub/usecases/PostUseCase.kt +++ b/android/app/src/main/java/com/goliath/emojihub/usecases/PostUseCase.kt @@ -1,8 +1,10 @@ package com.goliath.emojihub.usecases +import android.util.Log import androidx.paging.PagingData import androidx.paging.map import com.goliath.emojihub.data_sources.ApiErrorController +import com.goliath.emojihub.data_sources.CustomError import com.goliath.emojihub.models.Post import com.goliath.emojihub.models.PostDto import com.goliath.emojihub.models.UploadPostDto @@ -10,7 +12,9 @@ import com.goliath.emojihub.repositories.remote.PostRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import java.net.ConnectException import javax.inject.Inject import javax.inject.Singleton @@ -50,20 +54,44 @@ class PostUseCaseImpl @Inject constructor( } override suspend fun fetchPostList(): Flow> { - return repository.fetchPostList().map { it.map { dto -> Post(dto) } } + return try { + repository.fetchPostList().map { it.map { dto -> Post(dto) } } + } catch (e: ConnectException) { + errorController.setErrorState(CustomError.INTERNAL_SERVER_ERROR.statusCode) + flowOf(PagingData.empty()) + } catch (e: Exception) { + Log.e("PostUseCase", "Unknown Exception on fetchPostList: ${e.message}") + flowOf(PagingData.empty()) + } } override suspend fun fetchMyPostList(): Flow> { - return repository.fetchMyPostList().map { it.map { dto -> Post(dto) } } + return try { + repository.fetchMyPostList().map { it.map { dto -> Post(dto) } } + } catch (e: ConnectException) { + errorController.setErrorState(CustomError.INTERNAL_SERVER_ERROR.statusCode) + flowOf(PagingData.empty()) + } catch (e: Exception) { + Log.e("PostUseCase", "Unknown Exception on fetchMyPostList: ${e.message}") + flowOf(PagingData.empty()) + } } override suspend fun uploadPost(content: String): Boolean { val dto = UploadPostDto(content) - val response = repository.uploadPost(dto) - return if (response.isSuccessful) { - true - } else { - errorController.setErrorState(response.code()) + return try { + val response = repository.uploadPost(dto) + if (response.isSuccessful) { + true + } else { + errorController.setErrorState(response.code()) + false + } + } catch (e: ConnectException) { + errorController.setErrorState(CustomError.INTERNAL_SERVER_ERROR.statusCode) + false + } catch (e: Exception) { + Log.e("PostUseCase", "Unknown Exception on uploadPost: ${e.message}") false } } diff --git a/android/app/src/main/java/com/goliath/emojihub/usecases/ReactionUseCase.kt b/android/app/src/main/java/com/goliath/emojihub/usecases/ReactionUseCase.kt new file mode 100644 index 00000000..6ed5d23c --- /dev/null +++ b/android/app/src/main/java/com/goliath/emojihub/usecases/ReactionUseCase.kt @@ -0,0 +1,60 @@ +package com.goliath.emojihub.usecases + +import androidx.paging.PagingData +import androidx.paging.map +import com.goliath.emojihub.data_sources.ApiErrorController +import com.goliath.emojihub.models.ReactionWithEmoji +import com.goliath.emojihub.repositories.remote.ReactionRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Singleton + +interface ReactionUseCase { + + val reactionList: StateFlow> + suspend fun fetchReactionList(postId: String, emojiUnicode: String): Flow> + suspend fun updateReactionList(data: PagingData) + suspend fun uploadReaction(postId: String, emojiId: String): Boolean + suspend fun getReactionWithId(id: String) + suspend fun deleteReaction(reactionId: String) +} + +@Singleton +class ReactionUseCaseImpl @Inject constructor( + private val repository: ReactionRepository, + private val errorController: ApiErrorController +): ReactionUseCase { + + private val _reactionList = MutableStateFlow>(PagingData.empty()) + override val reactionList: StateFlow> + get() = _reactionList + + override suspend fun updateReactionList(data: PagingData) { + _reactionList.emit(data) + } + + override suspend fun fetchReactionList(postId: String, emojiUnicode: String): Flow> { + return repository.fetchReactionList(postId, emojiUnicode).map { it.map { dto -> ReactionWithEmoji(dto) } } + } + + override suspend fun uploadReaction(postId: String, emojiId: String): Boolean { + val response = repository.uploadReaction(postId, emojiId) + return if (response.isSuccessful) { + true + } else { + errorController.setErrorState(response.code()) + false + } + } + + override suspend fun getReactionWithId(id: String) { + repository.getReactionWithId(id) + } + + override suspend fun deleteReaction(reactionId: String) { + repository.deleteReaction(reactionId) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/usecases/UseCaseModule.kt b/android/app/src/main/java/com/goliath/emojihub/usecases/UseCaseModule.kt index d09a6075..5be128cc 100644 --- a/android/app/src/main/java/com/goliath/emojihub/usecases/UseCaseModule.kt +++ b/android/app/src/main/java/com/goliath/emojihub/usecases/UseCaseModule.kt @@ -16,4 +16,7 @@ abstract class UseCaseModule { @Binds abstract fun bindsPostUseCase(impl: PostUseCaseImpl): PostUseCase + + @Binds + abstract fun bindsReactionUseCase(impl: ReactionUseCaseImpl): ReactionUseCase } \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/usecases/UserUseCase.kt b/android/app/src/main/java/com/goliath/emojihub/usecases/UserUseCase.kt index 989df348..acc75f3e 100644 --- a/android/app/src/main/java/com/goliath/emojihub/usecases/UserUseCase.kt +++ b/android/app/src/main/java/com/goliath/emojihub/usecases/UserUseCase.kt @@ -3,22 +3,27 @@ package com.goliath.emojihub.usecases import android.util.Log import com.goliath.emojihub.EmojiHubApplication import com.goliath.emojihub.data_sources.ApiErrorController +import com.goliath.emojihub.data_sources.CustomError import com.goliath.emojihub.models.LoginUserDto import com.goliath.emojihub.models.RegisterUserDto import com.goliath.emojihub.models.User +import com.goliath.emojihub.models.UserDetails import com.goliath.emojihub.models.UserDto import com.goliath.emojihub.repositories.remote.UserRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import java.net.ConnectException import javax.inject.Inject import javax.inject.Singleton sealed interface UserUseCase { val accessTokenState: StateFlow val userState: StateFlow + val userDetailsState: StateFlow suspend fun fetchUser(id: String) + suspend fun fetchMyInfo() suspend fun registerUser(email: String, name: String, password: String): Boolean suspend fun login(name: String, password: String) suspend fun logout() @@ -39,59 +44,110 @@ class UserUseCaseImpl @Inject constructor( override val userState: StateFlow get() = _userState + private val _userDetailsState: MutableStateFlow = MutableStateFlow(null) + override val userDetailsState: StateFlow + get() = _userDetailsState + override suspend fun fetchUser(id: String) { repository.fetchUser(id) } + override suspend fun fetchMyInfo() { + val accessToken = EmojiHubApplication.preferences.accessToken ?: return + if (accessToken.isEmpty()) return + try { + val response = repository.fetchMyInfo(accessToken) + response.let { + if(it.isSuccessful) { + val userDetailsDto = it.body() ?: return // FIXME: may be considered as an error + _userDetailsState.update { UserDetails(userDetailsDto) } + } + else errorController.setErrorState(it.code()) + } + } catch (e: ConnectException) { + errorController.setErrorState(CustomError.INTERNAL_SERVER_ERROR.statusCode) + } catch (e: Exception) { + Log.e("UserUseCase", "Unknown Exception on fetchMyEmoji: ${e.message}") + } + } + override suspend fun registerUser(email: String, name: String, password: String): Boolean { val dto = RegisterUserDto(email, name, password) - val response = repository.registerUser(dto) - response.let { - if(it.isSuccessful) return true - else errorController.setErrorState(it.code()) + try { + val response = repository.registerUser(dto) + response.let { + if(it.isSuccessful) return true + else errorController.setErrorState(it.code()) + } + return response.isSuccessful + } catch (e: ConnectException) { + errorController.setErrorState(CustomError.INTERNAL_SERVER_ERROR.statusCode) + } catch (e: Exception) { + Log.e("UserUseCase", "Unknown Exception on registerUser: ${e.message}") } - return response.isSuccessful + return false } override suspend fun login(name: String, password: String) { val dto = LoginUserDto(name, password) - val response = repository.login(dto) - response.let { - if (it.isSuccessful) { - val accessToken = it.body()?.accessToken - _accessTokenState.update { accessToken } - _userState.update { User(UserDto(name)) } - EmojiHubApplication.preferences.accessToken = accessToken - } else { - errorController.setErrorState(it.code()) + try { + val response = repository.login(dto) + response.let { + if (it.isSuccessful) { + val accessToken = it.body()?.accessToken + _accessTokenState.update { accessToken } + _userState.update { User(UserDto(name)) } + EmojiHubApplication.preferences.accessToken = accessToken + EmojiHubApplication.preferences.currentUser = name + } else { + errorController.setErrorState(it.code()) + } } + } catch (e: ConnectException) { + errorController.setErrorState(CustomError.INTERNAL_SERVER_ERROR.statusCode) + } catch (e: Exception) { + Log.e("UserUseCase", "Unknown Exception on login: ${e.message}") } } override suspend fun logout() { - val response = repository.logout() - response.let { - if (it.isSuccessful) { - EmojiHubApplication.preferences.accessToken = null - _accessTokenState.update { null } - _userState.update { null } - } else { - errorController.setErrorState(it.code()) + try { + val response = repository.logout() + response.let { + if (it.isSuccessful) { + EmojiHubApplication.preferences.accessToken = null + EmojiHubApplication.preferences.currentUser = null + _accessTokenState.update { null } + _userState.update { null } + } else { + errorController.setErrorState(it.code()) + } } + } catch (e: ConnectException) { + errorController.setErrorState(CustomError.INTERNAL_SERVER_ERROR.statusCode) + } catch (e: Exception) { + Log.e("UserUseCase", "Unknown Exception on logout: ${e.message}") } } override suspend fun signOut() { val accessToken = EmojiHubApplication.preferences.accessToken ?: return - val response = repository.signOut(accessToken) - response.let { - if (it.isSuccessful) { - EmojiHubApplication.preferences.accessToken = null - _accessTokenState.update { null } - _userState.update { null } - } else { - errorController.setErrorState(it.code()) + try { + val response = repository.signOut(accessToken) + response.let { + if (it.isSuccessful) { + EmojiHubApplication.preferences.accessToken = null + EmojiHubApplication.preferences.currentUser = null + _accessTokenState.update { null } + _userState.update { null } + } else { + errorController.setErrorState(it.code()) + } } + } catch (e: ConnectException) { + errorController.setErrorState(CustomError.INTERNAL_SERVER_ERROR.statusCode) + } catch (e: Exception) { + Log.e("UserUseCase", "Unknown Exception on signOut: ${e.message}") } } } \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/viewmodels/EmojiViewModel.kt b/android/app/src/main/java/com/goliath/emojihub/viewmodels/EmojiViewModel.kt index 6ac0a5c8..0099c72a 100644 --- a/android/app/src/main/java/com/goliath/emojihub/viewmodels/EmojiViewModel.kt +++ b/android/app/src/main/java/com/goliath/emojihub/viewmodels/EmojiViewModel.kt @@ -3,6 +3,7 @@ package com.goliath.emojihub.viewmodels import android.net.Uri import android.util.Log import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel @@ -26,25 +27,29 @@ class EmojiViewModel @Inject constructor( private val emojiUseCase: EmojiUseCase ): ViewModel() { lateinit var videoUri: Uri - var currentEmoji: Emoji? = null + lateinit var currentEmoji: Emoji var bottomSheetContent by mutableStateOf(BottomSheetContent.EMPTY) - private val _saveEmojiState = MutableStateFlow?>(null) + // 0: not saved, 1: saved, -1: not changed + private val _saveEmojiState = MutableStateFlow(-1) val saveEmojiState = _saveEmojiState.asStateFlow() - private val _unSaveEmojiState = MutableStateFlow?>(null) + private val _unSaveEmojiState = MutableStateFlow(-1) val unSaveEmojiState = _unSaveEmojiState.asStateFlow() + var sortByDate by mutableIntStateOf(0) + val emojiList = emojiUseCase.emojiList val myCreatedEmojiList = emojiUseCase.myCreatedEmojiList val mySavedEmojiList = emojiUseCase.mySavedEmojiList + companion object { private const val _topK = 3 } fun fetchEmojiList() { viewModelScope.launch { - emojiUseCase.fetchEmojiList() + emojiUseCase.fetchEmojiList(sortByDate) .cachedIn(viewModelScope) .collect { emojiUseCase.updateEmojiList(it) @@ -86,15 +91,23 @@ class EmojiViewModel @Inject constructor( fun saveEmoji(id: String) { viewModelScope.launch { - val result = emojiUseCase.saveEmoji(id) - _saveEmojiState.value = result + val isSuccess = emojiUseCase.saveEmoji(id) + _saveEmojiState.value = if (isSuccess) 1 else 0 } } fun unSaveEmoji(id: String) { viewModelScope.launch { - val result = emojiUseCase.unSaveEmoji(id) - _unSaveEmojiState.value = result + val isSuccess = emojiUseCase.unSaveEmoji(id) + _unSaveEmojiState.value = if (isSuccess) 1 else 0 } } + + fun resetSaveEmojiState() { + _saveEmojiState.value = -1 + } + + fun resetUnSaveEmojiState() { + _unSaveEmojiState.value = -1 + } } \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/viewmodels/PostViewModel.kt b/android/app/src/main/java/com/goliath/emojihub/viewmodels/PostViewModel.kt index 06945776..161c52c8 100644 --- a/android/app/src/main/java/com/goliath/emojihub/viewmodels/PostViewModel.kt +++ b/android/app/src/main/java/com/goliath/emojihub/viewmodels/PostViewModel.kt @@ -1,8 +1,12 @@ package com.goliath.emojihub.viewmodels +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.cachedIn +import com.goliath.emojihub.models.Post import com.goliath.emojihub.usecases.PostUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @@ -15,6 +19,8 @@ class PostViewModel @Inject constructor( val postList = postUseCase.postList val myPostList = postUseCase.myPostList + var currentPostId by mutableStateOf("") + lateinit var currentPost: Post suspend fun fetchPostList() { viewModelScope.launch { diff --git a/android/app/src/main/java/com/goliath/emojihub/viewmodels/ReactionViewModel.kt b/android/app/src/main/java/com/goliath/emojihub/viewmodels/ReactionViewModel.kt new file mode 100644 index 00000000..ab5d67f1 --- /dev/null +++ b/android/app/src/main/java/com/goliath/emojihub/viewmodels/ReactionViewModel.kt @@ -0,0 +1,39 @@ +package com.goliath.emojihub.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.cachedIn +import com.goliath.emojihub.usecases.ReactionUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ReactionViewModel @Inject constructor( + private val reactionUseCase: ReactionUseCase +): ViewModel() { + + val reactionList = reactionUseCase.reactionList + + suspend fun fetchReactionList(postId: String, emojiUnicode: String) { + viewModelScope.launch { + reactionUseCase.fetchReactionList(postId, emojiUnicode) + .cachedIn(viewModelScope) + .collect { + reactionUseCase.updateReactionList(it) + } + } + } + + suspend fun uploadReaction(postId: String, emojiId: String): Boolean { + return reactionUseCase.uploadReaction(postId, emojiId) + } + + suspend fun getReactionWithId(id: String) { + reactionUseCase.getReactionWithId(id) + } + + suspend fun deleteReaction(reactionId: String) { + reactionUseCase.deleteReaction(reactionId) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/viewmodels/UserViewModel.kt b/android/app/src/main/java/com/goliath/emojihub/viewmodels/UserViewModel.kt index 81012500..ecc77209 100644 --- a/android/app/src/main/java/com/goliath/emojihub/viewmodels/UserViewModel.kt +++ b/android/app/src/main/java/com/goliath/emojihub/viewmodels/UserViewModel.kt @@ -13,11 +13,18 @@ class UserViewModel @Inject constructor( ): ViewModel() { val accessTokenState = userUseCase.accessTokenState val userState = userUseCase.userState + val userDetailsState = userUseCase.userDetailsState suspend fun fetchUser(id: String) { userUseCase.fetchUser(id) } + fun fetchMyInfo() { + viewModelScope.launch { + userUseCase.fetchMyInfo() + } + } + suspend fun login(username: String, password: String) { userUseCase.login(username, password) } diff --git a/android/app/src/main/java/com/goliath/emojihub/views/CreatePostPage.kt b/android/app/src/main/java/com/goliath/emojihub/views/CreatePostPage.kt index 1bb19f67..d20c29b1 100644 --- a/android/app/src/main/java/com/goliath/emojihub/views/CreatePostPage.kt +++ b/android/app/src/main/java/com/goliath/emojihub/views/CreatePostPage.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.launch @Composable fun CreatePostPage( - viewModel: PostViewModel + postViewModel: PostViewModel ) { val navController = LocalNavController.current @@ -42,7 +42,7 @@ fun CreatePostPage( ) { TextButton(onClick = { coroutineScope.launch { - val success = viewModel.uploadPost(content.text) + val success = postViewModel.uploadPost(content.text) if (success) { showSuccessDialog = true } diff --git a/android/app/src/main/java/com/goliath/emojihub/views/EmojiPage.kt b/android/app/src/main/java/com/goliath/emojihub/views/EmojiPage.kt index 2b2ac6db..6128af2c 100644 --- a/android/app/src/main/java/com/goliath/emojihub/views/EmojiPage.kt +++ b/android/app/src/main/java/com/goliath/emojihub/views/EmojiPage.kt @@ -1,50 +1,75 @@ package com.goliath.emojihub.views -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add import android.Manifest import android.content.pm.PackageManager -import android.os.Build +import android.media.MediaMetadataRetriever +import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement 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.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.goliath.emojihub.views.components.EmojiCell import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.compose.collectAsLazyPagingItems import com.goliath.emojihub.LocalNavController import com.goliath.emojihub.NavigationDestination +import com.goliath.emojihub.extensions.toEmoji +import com.goliath.emojihub.navigateAsOrigin +import com.goliath.emojihub.ui.theme.Color.Black import com.goliath.emojihub.ui.theme.Color.White import com.goliath.emojihub.viewmodels.EmojiViewModel +import com.goliath.emojihub.viewmodels.UserViewModel +import com.goliath.emojihub.views.components.CustomDialog +import com.goliath.emojihub.views.components.EmojiCell import com.goliath.emojihub.views.components.EmojiCellDisplay import com.goliath.emojihub.views.components.TopNavigationBar @Composable -fun EmojiPage( -) { +fun EmojiPage() { val context = LocalContext.current val navController = LocalNavController.current - val viewModel = hiltViewModel() + val userViewModel = hiltViewModel() + val emojiViewModel = hiltViewModel() + + val currentUser = userViewModel.userState.collectAsState().value + val emojiList = emojiViewModel.emojiList.collectAsLazyPagingItems() + + var showNonUserDialog by remember { mutableStateOf(false) } + var showVideoTooLongDialog by remember { mutableStateOf(false) } + var dropDownMenuExpanded by remember { mutableStateOf(false) } val permissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() @@ -55,31 +80,43 @@ fun EmojiPage( ) { uri -> if (uri != null) { Log.d("PhotoPicker", "Selected URI: $uri") - viewModel.videoUri = uri - navController.navigate(NavigationDestination.TransformVideo) + val retriever = MediaMetadataRetriever() + retriever.setDataSource(context, uri) + val duration = retriever.extractMetadata(METADATA_KEY_DURATION)?.toLongOrNull() ?: 0 + retriever.release() + if (duration >= 6000) { + showVideoTooLongDialog = true + } else { + emojiViewModel.videoUri = uri + navController.navigate(NavigationDestination.TransformVideo) + } } } - val emojiList = viewModel.emojiList.collectAsLazyPagingItems() + // 앱이 처음 실행될 때, 유저 정보를 가져오기 위함 + LaunchedEffect(userViewModel) { + userViewModel.fetchMyInfo() + } - LaunchedEffect(Unit) - { - viewModel.fetchEmojiList() + LaunchedEffect(Unit) { + emojiViewModel.fetchEmojiList() } Column(Modifier.background(White)) { TopNavigationBar("Emoji", shouldNavigate = false) { IconButton(onClick = { - when (PackageManager.PERMISSION_GRANTED) { - ContextCompat.checkSelfPermission( - context, Manifest.permission.READ_MEDIA_VIDEO - ) -> { - pickMediaLauncher.launch(PickVisualMediaRequest( - ActivityResultContracts.PickVisualMedia.VideoOnly - )) - } - else -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (currentUser == null) { + showNonUserDialog = true + } else { + when (PackageManager.PERMISSION_GRANTED) { + ContextCompat.checkSelfPermission( + context, Manifest.permission.READ_MEDIA_VIDEO + ) -> { + pickMediaLauncher.launch(PickVisualMediaRequest( + ActivityResultContracts.PickVisualMedia.VideoOnly + )) + } + else -> { permissionLauncher.launch(Manifest.permission.READ_MEDIA_VIDEO) } } @@ -95,7 +132,45 @@ fun EmojiPage( Column(Modifier.padding(horizontal = 16.dp)) { Spacer(Modifier.height(28.dp)) - Text("Trending 🔥", fontSize = 20.sp, fontWeight = FontWeight.Bold) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = if (emojiViewModel.sortByDate == 0) "Trending 🔥" else "Recently added " + "U+D83D U+DD52".toEmoji(), fontSize = 20.sp, fontWeight = FontWeight.Bold) + + Column { + Button( + onClick = { dropDownMenuExpanded = true }, + colors = ButtonDefaults.buttonColors( + backgroundColor = Black, + contentColor = White + ) + ) { + Text(text = "Sort by", fontSize = 12.sp) + } + + DropdownMenu( + expanded = dropDownMenuExpanded, + onDismissRequest = { dropDownMenuExpanded = false } + ) { + DropdownMenuItem(onClick = { + emojiViewModel.sortByDate = 1 + emojiViewModel.fetchEmojiList() + dropDownMenuExpanded = false + }) { + Text(text = "created date") + } + DropdownMenuItem(onClick = { + emojiViewModel.sortByDate = 0 + emojiViewModel.fetchEmojiList() + dropDownMenuExpanded = false + }) { + Text(text = "save count") + } + } + } + } LazyVerticalGrid( columns = GridCells.Fixed(2), @@ -106,12 +181,35 @@ fun EmojiPage( items(emojiList.itemCount) { index -> emojiList[index]?.let{ EmojiCell(emoji = it, displayMode = EmojiCellDisplay.VERTICAL) { selectedEmoji -> - viewModel.currentEmoji = selectedEmoji + emojiViewModel.currentEmoji = selectedEmoji navController.navigate(NavigationDestination.PlayEmojiVideo) } } } } } + + if (showVideoTooLongDialog) { + CustomDialog( + title = "안내", + body = "최대 5초 길이의 영상만 업로드할 수 있습니다.", + onDismissRequest = { showVideoTooLongDialog = false }, + confirm = { showVideoTooLongDialog = false } + ) + } + + if (showNonUserDialog) { + CustomDialog( + title = "비회원 모드", + body = "회원만 이모지를 생성할 수 있습니다. 로그인 화면으로 이동할까요?", + confirmText = "이동", + needsCancelButton = true, + onDismissRequest = { showNonUserDialog = false }, + dismiss = { showNonUserDialog = false }, + confirm = { + navController.navigateAsOrigin(NavigationDestination.Onboard) + } + ) + } } } \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/views/FeedPage.kt b/android/app/src/main/java/com/goliath/emojihub/views/FeedPage.kt index d444e40b..a81a9b63 100644 --- a/android/app/src/main/java/com/goliath/emojihub/views/FeedPage.kt +++ b/android/app/src/main/java/com/goliath/emojihub/views/FeedPage.kt @@ -13,6 +13,11 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material3.Divider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -21,13 +26,14 @@ import com.goliath.emojihub.LocalBottomSheetController import com.goliath.emojihub.LocalNavController import com.goliath.emojihub.NavigationDestination import com.goliath.emojihub.models.createDummyEmoji +import com.goliath.emojihub.navigateAsOrigin import com.goliath.emojihub.ui.theme.Color import com.goliath.emojihub.ui.theme.Color.EmojiHubDividerColor import com.goliath.emojihub.viewmodels.EmojiViewModel import com.goliath.emojihub.viewmodels.PostViewModel -import com.goliath.emojihub.views.components.EmojiCell -import com.goliath.emojihub.views.components.EmojiCellDisplay +import com.goliath.emojihub.viewmodels.UserViewModel import com.goliath.emojihub.views.components.CustomBottomSheet +import com.goliath.emojihub.views.components.CustomDialog import com.goliath.emojihub.views.components.PostCell import com.goliath.emojihub.views.components.TopNavigationBar @@ -38,10 +44,20 @@ fun FeedPage() { val emojiViewModel = hiltViewModel() val postViewModel = hiltViewModel() + val userViewModel = hiltViewModel() + + val currentUser = userViewModel.userState.collectAsState().value val emojiList = (1..10).map { createDummyEmoji() } val postList = postViewModel.postList.collectAsLazyPagingItems() + var showNonUserDialog by remember { mutableStateOf(false) } + + // 앱이 처음 실행될 때, 유저 정보를 가져오기 위함 + LaunchedEffect(userViewModel) { + userViewModel.fetchMyInfo() + } + LaunchedEffect(Unit) { postViewModel.fetchPostList() } @@ -51,7 +67,11 @@ fun FeedPage() { ) { TopNavigationBar("Feed", shouldNavigate = false) { IconButton(onClick = { - navController.navigate(NavigationDestination.CreatePost) + if (currentUser == null) { + showNonUserDialog = true + } else { + navController.navigate(NavigationDestination.CreatePost) + } }) { Icon( imageVector = Icons.Default.Add, @@ -60,18 +80,14 @@ fun FeedPage() { } } - Box( - modifier = Modifier - .weight(1f) - .fillMaxWidth() - ) { + Box(modifier = Modifier.weight(1f).fillMaxWidth()) { LazyColumn( modifier = Modifier.fillMaxWidth(), contentPadding = PaddingValues(horizontal = 16.dp) ) { items(postList.itemCount) { index -> postList[index]?.let { - PostCell(post = it) + PostCell(post = it, isNonUser = currentUser == null) Divider(color = EmojiHubDividerColor, thickness = 0.5.dp) } } @@ -79,13 +95,23 @@ fun FeedPage() { } } + if (showNonUserDialog) { + CustomDialog( + title = "비회원 모드", + body = "회원만 글을 작성할 수 있습니다. 로그인 화면으로 이동할까요?", + confirmText = "이동", + needsCancelButton = true, + onDismissRequest = { showNonUserDialog = false }, + dismiss = { showNonUserDialog = false }, + confirm = { + navController.navigateAsOrigin(NavigationDestination.Onboard) + } + ) + } + if (bottomSheetController.isVisible) { CustomBottomSheet( - bottomSheetContent = emojiViewModel.bottomSheetContent, - emojiList = emojiList - ) { emoji -> - emojiViewModel.currentEmoji = emoji - navController.navigate(NavigationDestination.PlayEmojiVideo) - } + bottomSheetContent = emojiViewModel.bottomSheetContent + ) } } \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/views/LoginPage.kt b/android/app/src/main/java/com/goliath/emojihub/views/LoginPage.kt index ef675dec..98596b81 100644 --- a/android/app/src/main/java/com/goliath/emojihub/views/LoginPage.kt +++ b/android/app/src/main/java/com/goliath/emojihub/views/LoginPage.kt @@ -18,6 +18,8 @@ import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.material3.ButtonDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -34,9 +36,11 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import com.goliath.emojihub.LocalBottomNavigationController import com.goliath.emojihub.LocalNavController import com.goliath.emojihub.NavigationDestination import com.goliath.emojihub.R +import com.goliath.emojihub.navigateAsOrigin import com.goliath.emojihub.ui.theme.Color import com.goliath.emojihub.viewmodels.UserViewModel import com.goliath.emojihub.views.components.UnderlinedTextField @@ -47,20 +51,28 @@ fun LoginPage() { var username by remember { mutableStateOf(TextFieldValue("")) } var password by remember { mutableStateOf(TextFieldValue("")) } + val isLoginButtonDisabled by remember { derivedStateOf { username.text.isEmpty() || password.text.isEmpty() }} + val focusManager = LocalFocusManager.current val interactionSource = remember { MutableInteractionSource() } val userViewModel = hiltViewModel() val coroutineScope = rememberCoroutineScope() + val navController = LocalNavController.current + val bottomNavigationController = LocalBottomNavigationController.current + + LaunchedEffect(Unit) { + bottomNavigationController.updateDestination(PageItem.Feed) + } Box(modifier = Modifier - .background(Color.White) - .fillMaxSize() - .padding(horizontal = 16.dp) - .clickable(interactionSource = interactionSource, indication = null) { - focusManager.clearFocus() - }, + .background(Color.White) + .fillMaxSize() + .padding(horizontal = 16.dp) + .clickable(interactionSource = interactionSource, indication = null) { + focusManager.clearFocus() + }, contentAlignment = Alignment.Center ) { Column( @@ -94,8 +106,13 @@ fun LoginPage() { coroutineScope.launch { userViewModel.login(username.text, password.text) } + userViewModel.fetchMyInfo() }, - modifier = Modifier.padding(top = 24.dp).fillMaxWidth().height(44.dp), + modifier = Modifier + .padding(top = 24.dp) + .fillMaxWidth() + .height(44.dp), + enabled = !isLoginButtonDisabled, shape = RoundedCornerShape(50), colors = ButtonDefaults.buttonColors( containerColor = Color.Black, @@ -112,7 +129,9 @@ fun LoginPage() { Spacer(modifier = Modifier.height(8.dp)) OutlinedButton( onClick = { navController.navigate(NavigationDestination.SignUp) }, - modifier = Modifier.fillMaxWidth().height(44.dp), + modifier = Modifier + .fillMaxWidth() + .height(44.dp), shape = RoundedCornerShape(50), border = BorderStroke(1.dp, Color.Black), colors = ButtonDefaults.buttonColors( @@ -133,7 +152,7 @@ fun LoginPage() { color = Color.DarkGray, style = TextStyle(textDecoration = TextDecoration.Underline), modifier = Modifier.clickable { - navController.navigate(NavigationDestination.MainPage) + navController.navigateAsOrigin(NavigationDestination.MainPage) } ) Spacer(modifier = Modifier.height(24.dp)) diff --git a/android/app/src/main/java/com/goliath/emojihub/views/MainPage.kt b/android/app/src/main/java/com/goliath/emojihub/views/MainPage.kt index c6cdd681..875ea166 100644 --- a/android/app/src/main/java/com/goliath/emojihub/views/MainPage.kt +++ b/android/app/src/main/java/com/goliath/emojihub/views/MainPage.kt @@ -31,9 +31,7 @@ fun MainPage() { pageItemList.forEach { pageItem -> BottomNavigationItem( selected = currentRoute == pageItem.screenRoute, - onClick = { - bottomNavigationController.updateDestination(pageItem) - }, + onClick = { bottomNavigationController.updateDestination(pageItem) }, icon = { Icon( painter = painterResource(id = pageItem.icon), diff --git a/android/app/src/main/java/com/goliath/emojihub/views/ProfilePage.kt b/android/app/src/main/java/com/goliath/emojihub/views/ProfilePage.kt index 5e473e12..6f9c4b92 100644 --- a/android/app/src/main/java/com/goliath/emojihub/views/ProfilePage.kt +++ b/android/app/src/main/java/com/goliath/emojihub/views/ProfilePage.kt @@ -29,6 +29,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.compose.collectAsLazyPagingItems import com.goliath.emojihub.LocalNavController import com.goliath.emojihub.NavigationDestination +import com.goliath.emojihub.navigateAsOrigin import com.goliath.emojihub.ui.theme.Color import com.goliath.emojihub.ui.theme.Color.EmojiHubDetailLabel import com.goliath.emojihub.ui.theme.Color.White @@ -62,6 +63,11 @@ fun ProfilePage() { var showLogoutDialog by remember { mutableStateOf(false) } var showSignOutDialog by remember { mutableStateOf(false) } + // 앱이 처음 실행될 때, 유저 정보를 가져오기 위함 + LaunchedEffect(userViewModel) { + userViewModel.fetchMyInfo() + } + LaunchedEffect(Unit) { postViewModel.fetchMyPostList() emojiViewModel.fetchMyCreatedEmojiList() @@ -125,11 +131,14 @@ fun ProfilePage() { navigateToDestination = { navController.navigate(NavigationDestination.MyEmojiList) } ) { items(myCreatedEmojiList.itemCount) { index -> - myCreatedEmojiList[index]?.let { + myCreatedEmojiList[index]?.let { emoji -> EmojiCell( - emoji = it, + emoji = emoji, displayMode = EmojiCellDisplay.HORIZONTAL, - onSelected = {}) + onSelected = { + emojiViewModel.currentEmoji = emoji + navController.navigate(NavigationDestination.PlayEmojiVideo) + }) } } } @@ -142,11 +151,14 @@ fun ProfilePage() { navigateToDestination = { navController.navigate(NavigationDestination.MySavedEmojiList) } ) { items(mySavedEmojiList.itemCount) { index -> - mySavedEmojiList[index]?.let { + mySavedEmojiList[index]?.let { emoji -> EmojiCell( - emoji = it, + emoji = emoji, displayMode = EmojiCellDisplay.HORIZONTAL, - onSelected = {}) + onSelected = { + emojiViewModel.currentEmoji = emoji + navController.navigate(NavigationDestination.PlayEmojiVideo) + }) } } } @@ -173,7 +185,10 @@ fun ProfilePage() { needsCancelButton = true, onDismissRequest = { showLogoutDialog = false }, dismiss = { showLogoutDialog = false }, - confirm = { userViewModel.logout() } + confirm = { + navController.navigateAsOrigin(NavigationDestination.Onboard) + userViewModel.logout() + } ) } @@ -186,7 +201,10 @@ fun ProfilePage() { needsCancelButton = true, onDismissRequest = { showSignOutDialog = false }, dismiss = { showSignOutDialog = false }, - confirm = { userViewModel.signOut() } + confirm = { + navController.navigateAsOrigin(NavigationDestination.Onboard) + userViewModel.signOut() + } ) } } diff --git a/android/app/src/main/java/com/goliath/emojihub/views/SignUpPage.kt b/android/app/src/main/java/com/goliath/emojihub/views/SignUpPage.kt index d4c9f7ad..60d96744 100644 --- a/android/app/src/main/java/com/goliath/emojihub/views/SignUpPage.kt +++ b/android/app/src/main/java/com/goliath/emojihub/views/SignUpPage.kt @@ -29,6 +29,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import com.goliath.emojihub.LocalNavController +import com.goliath.emojihub.NavigationDestination +import com.goliath.emojihub.navigateAsOrigin import com.goliath.emojihub.ui.theme.Color import com.goliath.emojihub.viewmodels.UserViewModel import com.goliath.emojihub.views.components.CustomDialog @@ -121,7 +123,10 @@ fun SignUpPage() { if (showDialog) { CustomDialog( title = "완료", - body = "계정 생성이 완료되었습니다." + body = "계정 생성이 완료되었습니다.", + onDismissRequest = { showDialog = false }, + dismiss = { showDialog = false }, + confirm = { navController.navigateAsOrigin(NavigationDestination.Login) } ) } } diff --git a/android/app/src/main/java/com/goliath/emojihub/views/TransformVideoPage.kt b/android/app/src/main/java/com/goliath/emojihub/views/TransformVideoPage.kt index eefa60c2..8263f562 100644 --- a/android/app/src/main/java/com/goliath/emojihub/views/TransformVideoPage.kt +++ b/android/app/src/main/java/com/goliath/emojihub/views/TransformVideoPage.kt @@ -29,6 +29,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.runtime.mutableStateOf @@ -74,6 +75,13 @@ fun TransformVideoPage( } } + DisposableEffect(Unit) { + onDispose { + exoPlayer.stop() + exoPlayer.release() + } + } + var createdEmojiList by remember { mutableStateOf>(emptyList()) } Scaffold( diff --git a/android/app/src/main/java/com/goliath/emojihub/views/components/CreatedEmojiListView.kt b/android/app/src/main/java/com/goliath/emojihub/views/components/CreatedEmojiListView.kt index 4fbade49..af9cf1f9 100644 --- a/android/app/src/main/java/com/goliath/emojihub/views/components/CreatedEmojiListView.kt +++ b/android/app/src/main/java/com/goliath/emojihub/views/components/CreatedEmojiListView.kt @@ -1,21 +1,52 @@ package com.goliath.emojihub.views.components import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.paging.compose.collectAsLazyPagingItems import com.goliath.emojihub.LocalNavController +import com.goliath.emojihub.NavigationDestination import com.goliath.emojihub.ui.theme.Color +import com.goliath.emojihub.viewmodels.EmojiViewModel @Composable fun CreatedEmojiListView( - + emojiViewModel: EmojiViewModel ) { val navController = LocalNavController.current + val emojiList = emojiViewModel.myCreatedEmojiList.collectAsLazyPagingItems() + Column ( Modifier.background(Color.White) ) { - TopNavigationBar(navigate = { navController.popBackStack() }) + TopNavigationBar( + title = "내가 만든 이모지", + navigate = { navController.popBackStack() } + ) + + Column(Modifier.padding(horizontal = 16.dp)) { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier.padding(top = 18.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + items(emojiList.itemCount) { index -> + emojiList[index]?.let{ + EmojiCell(emoji = it, displayMode = EmojiCellDisplay.VERTICAL) { selectedEmoji -> + emojiViewModel.currentEmoji = selectedEmoji + navController.navigate(NavigationDestination.PlayEmojiVideo) + } + } + } + } + } } } \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/views/components/CreatedPostListView.kt b/android/app/src/main/java/com/goliath/emojihub/views/components/CreatedPostListView.kt index 59a0f116..f918dd5d 100644 --- a/android/app/src/main/java/com/goliath/emojihub/views/components/CreatedPostListView.kt +++ b/android/app/src/main/java/com/goliath/emojihub/views/components/CreatedPostListView.kt @@ -1,21 +1,63 @@ package com.goliath.emojihub.views.components import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Divider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.paging.PagingData +import androidx.paging.compose.collectAsLazyPagingItems +import com.goliath.emojihub.LocalBottomSheetController import com.goliath.emojihub.LocalNavController +import com.goliath.emojihub.models.Post import com.goliath.emojihub.ui.theme.Color +import com.goliath.emojihub.viewmodels.EmojiViewModel +import kotlinx.coroutines.flow.StateFlow @Composable fun CreatedPostListView( - + postList: StateFlow> ) { val navController = LocalNavController.current + val bottomSheetController = LocalBottomSheetController.current + + val emojiViewModel = hiltViewModel() + + val pagingPostList = postList.collectAsLazyPagingItems() + + Column (Modifier.background(Color.White)) { + TopNavigationBar( + title = "내가 작성한 포스트", + navigate = { navController.popBackStack() } + ) + + Box( + Modifier + .weight(1f) + .fillMaxWidth()) { + LazyColumn( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 16.dp) + ) { + items(pagingPostList.itemCount) { index -> + pagingPostList[index]?.let { + PostCell(post = it) + Divider(color = Color.EmojiHubDividerColor, thickness = 0.5.dp) + } + } + } + } + } - Column ( - Modifier.background(Color.White) - ) { - TopNavigationBar(navigate = { navController.popBackStack() }) + if (bottomSheetController.isVisible) { + CustomBottomSheet( + bottomSheetContent = emojiViewModel.bottomSheetContent + ) } } \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/views/components/CustomBottomSheet.kt b/android/app/src/main/java/com/goliath/emojihub/views/components/CustomBottomSheet.kt index 4adb5b38..b5938eb8 100644 --- a/android/app/src/main/java/com/goliath/emojihub/views/components/CustomBottomSheet.kt +++ b/android/app/src/main/java/com/goliath/emojihub/views/components/CustomBottomSheet.kt @@ -1,6 +1,5 @@ package com.goliath.emojihub.views.components -import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -11,7 +10,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ButtonDefaults import androidx.compose.material.OutlinedButton @@ -36,13 +34,15 @@ import androidx.paging.compose.collectAsLazyPagingItems import com.goliath.emojihub.LocalBottomSheetController import com.goliath.emojihub.LocalNavController import com.goliath.emojihub.NavigationDestination -import com.goliath.emojihub.ui.theme.Color.EmojiHubDividerColor import com.goliath.emojihub.extensions.toEmoji import com.goliath.emojihub.models.Emoji +import com.goliath.emojihub.ui.theme.Color.EmojiHubDividerColor import com.goliath.emojihub.ui.theme.Color.LightGray -import kotlinx.coroutines.launch import com.goliath.emojihub.ui.theme.Color.White import com.goliath.emojihub.viewmodels.EmojiViewModel +import com.goliath.emojihub.viewmodels.PostViewModel +import com.goliath.emojihub.viewmodels.ReactionViewModel +import kotlinx.coroutines.launch enum class BottomSheetContent { VIEW_REACTION, ADD_REACTION, EMPTY @@ -51,27 +51,36 @@ enum class BottomSheetContent { @OptIn(ExperimentalMaterial3Api::class) @Composable fun CustomBottomSheet ( - bottomSheetContent: BottomSheetContent, - emojiList: List, - emojiCellClicked: (Emoji) -> Unit + bottomSheetContent: BottomSheetContent ){ val bottomSheetState = LocalBottomSheetController.current val coroutineScope = rememberCoroutineScope() val viewModel = hiltViewModel() - val navController = LocalNavController.current - var selectedEmojiClass by remember { mutableStateOf("전체") } - val emojisByClass = emojiList.groupBy { it.unicode } - val emojiClassFilters = listOf("전체") + emojisByClass.keys.toList() - val emojiCounts = emojisByClass.mapValues { it.value.size } + val reactionViewModel = hiltViewModel() + val postViewModel = hiltViewModel() + val navController = LocalNavController.current val myCreatedEmojiList = viewModel.myCreatedEmojiList.collectAsLazyPagingItems() val mySavedEmojiList = viewModel.mySavedEmojiList.collectAsLazyPagingItems() + val reactionList = reactionViewModel.reactionList.collectAsLazyPagingItems() var displayMyCreatedEmojis by remember { mutableStateOf(true) } - LaunchedEffect(Unit) { - viewModel.fetchMyCreatedEmojiList() - viewModel.fetchMySavedEmojiList() + var selectedEmojiClass by remember { mutableStateOf("전체") } + val emojisByUnicode = postViewModel.currentPost.reaction.groupBy { it.emoji_unicode } + val emojiUnicodeFilters = listOf("전체") + emojisByUnicode.keys.toList() + val emojiCounts = emojisByUnicode.mapValues { it.value.size } + var selectedEmojiUnicode by remember { mutableStateOf("") } + + LaunchedEffect(selectedEmojiUnicode) { + reactionViewModel.fetchReactionList(postViewModel.currentPostId, selectedEmojiUnicode) + } + + LaunchedEffect(bottomSheetContent) { + if(bottomSheetContent == BottomSheetContent.ADD_REACTION) { + viewModel.fetchMyCreatedEmojiList() + viewModel.fetchMySavedEmojiList() + } } ModalBottomSheet( @@ -93,16 +102,17 @@ fun CustomBottomSheet ( verticalAlignment = Alignment.CenterVertically ) { EmojiClassFilterRow( - emojiClass = emojiClassFilters, + emojiClass = emojiUnicodeFilters, emojiCounts = emojiCounts, onEmojiClassSelected = { selectedEmojiClass = it} ) { - items(emojiClassFilters.size) { emojiClass -> + items(emojiUnicodeFilters.size) { unicode -> EmojiClassFilterButton( - text = if (emojiClassFilters[emojiClass] == "전체") "전체" else "${emojiClassFilters[emojiClass].toEmoji()}${emojiCounts[emojiClassFilters[emojiClass]]}", - isSelected = emojiClassFilters[emojiClass] == selectedEmojiClass, + text = if (emojiUnicodeFilters[unicode] == "전체") "전체" else "${emojiUnicodeFilters[unicode].toEmoji()}${emojiCounts[emojiUnicodeFilters[unicode]]}", + isSelected = emojiUnicodeFilters[unicode] == selectedEmojiClass, onSelected = { - selectedEmojiClass = emojiClassFilters[emojiClass] + selectedEmojiClass = emojiUnicodeFilters[unicode] + selectedEmojiUnicode = if (emojiUnicodeFilters[unicode] == "전체") "" else emojiUnicodeFilters[unicode] } ) } @@ -174,10 +184,16 @@ fun CustomBottomSheet ( BottomSheetContent.EMPTY -> {} BottomSheetContent.VIEW_REACTION -> { - items(if (selectedEmojiClass == "전체") emojiList else emojiList.filter { it.unicode == selectedEmojiClass }, key = { it.id }) { emoji -> - EmojiCell(emoji = emoji, displayMode = EmojiCellDisplay.VERTICAL) {selectedEmoji -> - viewModel.currentEmoji = selectedEmoji - navController.navigate(NavigationDestination.PlayEmojiVideo) //FIXME: make a new destination or fix PlayEmojiVideo's back stack to include BottomSheet + items(reactionList.itemCount) { index -> + reactionList[index]?.let { + val emojiDto = it.emojiDto + val reactedBy = it.createdBy + if (emojiDto != null){ + ReactionEmojiCell(emoji = Emoji(emojiDto), reactedBy = reactedBy) { selectedEmoji -> + viewModel.currentEmoji = selectedEmoji + navController.navigate(NavigationDestination.PlayEmojiVideo) + } + } } } } @@ -187,7 +203,15 @@ fun CustomBottomSheet ( items(myCreatedEmojiList.itemCount) { index -> myCreatedEmojiList[index]?.let { EmojiCell(emoji = it, displayMode = EmojiCellDisplay.VERTICAL) { - //TODO: add reaction to post + coroutineScope.launch { + val success = reactionViewModel.uploadReaction(postId = postViewModel.currentPostId, emojiId = it.id) + if (success) { + coroutineScope.launch { + bottomSheetState.hide() + } + postViewModel.fetchPostList() + } + } } } } @@ -195,7 +219,15 @@ fun CustomBottomSheet ( items(mySavedEmojiList.itemCount) { index -> mySavedEmojiList[index]?.let { EmojiCell(emoji = it, displayMode = EmojiCellDisplay.VERTICAL) { - //TODO: add emoji reaction to post + coroutineScope.launch { + val success = reactionViewModel.uploadReaction(postId = postViewModel.currentPostId, emojiId = it.id) + if (success) { + coroutineScope.launch { + bottomSheetState.hide() + } + postViewModel.fetchPostList() + } + } } } } diff --git a/android/app/src/main/java/com/goliath/emojihub/views/components/EmptyProfile.kt b/android/app/src/main/java/com/goliath/emojihub/views/components/EmptyProfile.kt index 4e057e39..5465dabd 100644 --- a/android/app/src/main/java/com/goliath/emojihub/views/components/EmptyProfile.kt +++ b/android/app/src/main/java/com/goliath/emojihub/views/components/EmptyProfile.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.goliath.emojihub.LocalNavController import com.goliath.emojihub.NavigationDestination +import com.goliath.emojihub.navigateAsOrigin import com.goliath.emojihub.ui.theme.Color @Composable @@ -54,7 +55,7 @@ fun EmptyProfile() { } Spacer(modifier = Modifier.weight(1f)) Button( - onClick = { navController.navigate(NavigationDestination.Onboard) }, + onClick = { navController.navigateAsOrigin(NavigationDestination.Onboard) }, modifier = Modifier.fillMaxWidth().height(44.dp), shape = RoundedCornerShape(50), colors = ButtonDefaults.buttonColors( diff --git a/android/app/src/main/java/com/goliath/emojihub/views/components/PlayEmojiView.kt b/android/app/src/main/java/com/goliath/emojihub/views/components/PlayEmojiView.kt index 5327e414..d0a46eda 100644 --- a/android/app/src/main/java/com/goliath/emojihub/views/components/PlayEmojiView.kt +++ b/android/app/src/main/java/com/goliath/emojihub/views/components/PlayEmojiView.kt @@ -19,10 +19,11 @@ import androidx.compose.material.icons.filled.FileDownload import androidx.compose.material.icons.filled.FileDownloadOff import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -33,29 +34,46 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.Observer +import androidx.lifecycle.asLiveData import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView import com.goliath.emojihub.LocalNavController +import com.goliath.emojihub.NavigationDestination import com.goliath.emojihub.extensions.toEmoji +import com.goliath.emojihub.models.Emoji +import com.goliath.emojihub.models.User +import com.goliath.emojihub.models.UserDetails +import com.goliath.emojihub.navigateAsOrigin import com.goliath.emojihub.ui.theme.Color import com.goliath.emojihub.viewmodels.EmojiViewModel -import kotlinx.coroutines.launch +import com.goliath.emojihub.viewmodels.UserViewModel +@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) @Composable fun PlayEmojiView( - viewModel: EmojiViewModel + emojiViewModel: EmojiViewModel, + userViewModel: UserViewModel ) { - // Play video val context = LocalContext.current val navController = LocalNavController.current - val currentEmoji = viewModel.currentEmoji!! + val currentEmoji = emojiViewModel.currentEmoji + val currentUser = userViewModel.userState.collectAsState().value + val currentUserDetails = userViewModel.userDetailsState.collectAsState().value - var savedCount by remember { mutableStateOf(currentEmoji.savedCount) } - var isSaved by remember { mutableStateOf(currentEmoji.isSaved) } + var savedCount by remember { mutableIntStateOf(currentEmoji.savedCount) } + var isSavedEmoji by remember { mutableStateOf(checkEmojiHasSaved(currentUserDetails, currentEmoji)) } + val isCreatedEmoji by remember { mutableStateOf(checkEmojiHasCreated(currentUser, currentEmoji)) } + + val isSaveSuccess = emojiViewModel.saveEmojiState.asLiveData() + val isUnSaveSuccess = emojiViewModel.unSaveEmojiState.asLiveData() + var showNonUserDialog by remember { mutableStateOf(false) } var showUnSaveDialog by remember { mutableStateOf(false) } + var showCreatedEmojiDialog by remember { mutableStateOf(false) } val exoPlayer = remember { ExoPlayer.Builder(context).build().apply { @@ -69,6 +87,7 @@ fun PlayEmojiView( onDispose { exoPlayer.stop() exoPlayer.release() + userViewModel.fetchMyInfo() } } @@ -85,6 +104,7 @@ fun PlayEmojiView( modifier = Modifier.fillMaxSize(), factory = { PlayerView(it).apply { + this.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL player = exoPlayer } } @@ -93,6 +113,7 @@ fun PlayEmojiView( TopNavigationBar( title = "@" + currentEmoji.createdBy, largeTitle = false, + needsElevation = false, navigate = { navController.popBackStack() } ) {} @@ -106,20 +127,17 @@ fun PlayEmojiView( IconButton( modifier = Modifier.size(40.dp), onClick = { - if (isSaved) { - showUnSaveDialog = true - Toast.makeText(context, "Emoji unsaved!", Toast.LENGTH_SHORT).show() - } else { - viewModel.saveEmoji(currentEmoji.id) - isSaved = true - savedCount ++ - Toast.makeText(context, "Emoji saved!", Toast.LENGTH_SHORT).show() + when { + currentUser == null -> showNonUserDialog = true + isSavedEmoji -> showUnSaveDialog = true + isCreatedEmoji -> showCreatedEmojiDialog = true + else -> emojiViewModel.saveEmoji(currentEmoji.id) } } ) { Icon( imageVector = - if (isSaved) { + if (isSavedEmoji) { Icons.Default.FileDownloadOff } else { Icons.Default.FileDownload @@ -149,10 +167,50 @@ fun PlayEmojiView( fontSize = 28.sp, ) } - Spacer(modifier = Modifier.padding(bottom = 32.dp)) } } + + val saveEmojiStateObserver = Observer { + if (it == 1) { + isSavedEmoji = true + savedCount++ + Toast.makeText(context, "Emoji saved!", Toast.LENGTH_SHORT).show() + } else if (it == 0) { + Toast.makeText(context, "Emoji save failed!", Toast.LENGTH_SHORT).show() + } + emojiViewModel.resetSaveEmojiState() + } + + val unSaveEmojiStateObserver = Observer { + if (it == 1) { + isSavedEmoji = false + savedCount-- + showUnSaveDialog = false + Toast.makeText(context, "Emoji unsaved!", Toast.LENGTH_SHORT).show() + } else if (it == 0) { + showUnSaveDialog = false + Toast.makeText(context, "Emoji unsave failed!", Toast.LENGTH_SHORT).show() + } + emojiViewModel.resetUnSaveEmojiState() + } + + isSaveSuccess.observe(navController.currentBackStackEntry!!, saveEmojiStateObserver) + isUnSaveSuccess.observe(navController.currentBackStackEntry!!, unSaveEmojiStateObserver) + + if (showNonUserDialog) { + CustomDialog( + title = "비회원 모드", + body = "회원만 이모지를 저장할 수 있습니다. 로그인 화면으로 이동할까요?", + confirmText = "이동", + needsCancelButton = true, + onDismissRequest = { showNonUserDialog = false }, + dismiss = { showNonUserDialog = false }, + confirm = { + navController.navigateAsOrigin(NavigationDestination.Onboard) + } + ) + } if (showUnSaveDialog) { CustomDialog( @@ -162,13 +220,35 @@ fun PlayEmojiView( isDestructive = true, needsCancelButton = true, onDismissRequest = { showUnSaveDialog = false }, - confirm = { - viewModel.unSaveEmoji(currentEmoji.id) - isSaved = false - savedCount -- - showUnSaveDialog = false }, + confirm = { emojiViewModel.unSaveEmoji(currentEmoji.id) }, dismiss = { showUnSaveDialog = false } ) } + + if (showCreatedEmojiDialog) { + CustomDialog( + title = "내가 만든 이모지", + body = "내가 만든 이모지는 저장할 수 없습니다.", + confirmText = "확인", + needsCancelButton = false, + onDismissRequest = { showCreatedEmojiDialog = false }, + confirm = { showCreatedEmojiDialog = false } + ) + } } +} + +fun checkEmojiHasSaved(currentUserDetails: UserDetails?, currentEmoji: Emoji): Boolean { + if (currentUserDetails == null) return false + Log.d("checkEmojiHasSaved", "currentUserDetails.savedEmojiList: ${currentUserDetails.savedEmojiList}") + Log.d("checkEmojiHasSaved", "currentEmoji.id: ${currentEmoji.id}") + if (currentUserDetails.savedEmojiList?.contains(currentEmoji.id) == true) + return true + return false +} + +fun checkEmojiHasCreated(currentUser: User?, currentEmoji: Emoji): Boolean { + if (currentUser == null) return false + if (currentUser.name == currentEmoji.createdBy) return true + return false } \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/views/components/PostCell.kt b/android/app/src/main/java/com/goliath/emojihub/views/components/PostCell.kt index a388bb3c..b27805d7 100644 --- a/android/app/src/main/java/com/goliath/emojihub/views/components/PostCell.kt +++ b/android/app/src/main/java/com/goliath/emojihub/views/components/PostCell.kt @@ -15,7 +15,11 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AddReaction import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight @@ -23,19 +27,28 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import com.goliath.emojihub.LocalBottomSheetController +import com.goliath.emojihub.LocalNavController +import com.goliath.emojihub.NavigationDestination import com.goliath.emojihub.extensions.reactionsToString import com.goliath.emojihub.models.Post +import com.goliath.emojihub.navigateAsOrigin import com.goliath.emojihub.ui.theme.Color.EmojiHubDetailLabel import com.goliath.emojihub.viewmodels.EmojiViewModel +import com.goliath.emojihub.viewmodels.PostViewModel import kotlinx.coroutines.launch @Composable fun PostCell( - post: Post + post: Post, + isNonUser: Boolean = false ) { + val navController = LocalNavController.current val bottomSheetState = LocalBottomSheetController.current val coroutineScope = rememberCoroutineScope() val emojiViewModel = hiltViewModel() + val postViewModel = hiltViewModel() + + var showNonUserDialog by remember { mutableStateOf(false) } Box( modifier = Modifier @@ -79,13 +92,15 @@ fun PostCell( onClick = { coroutineScope.launch { emojiViewModel.bottomSheetContent = BottomSheetContent.VIEW_REACTION + postViewModel.currentPostId = post.id + postViewModel.currentPost = post bottomSheetState.show() } }, ) { Text( - text = reactionsToString(post.reaction), //TODO: Replace with reaction_unicode sent from backend + text = reactionsToString(post.reaction), fontSize = 13.sp, color = EmojiHubDetailLabel ) @@ -99,9 +114,15 @@ fun PostCell( } IconButton(onClick = { - coroutineScope.launch { - emojiViewModel.bottomSheetContent = BottomSheetContent.ADD_REACTION - bottomSheetState.show() + if (isNonUser) { + showNonUserDialog = true + } else { + coroutineScope.launch { + emojiViewModel.bottomSheetContent = BottomSheetContent.ADD_REACTION + postViewModel.currentPostId = post.id + postViewModel.currentPost = post + bottomSheetState.show() + } } }) { Icon( @@ -111,5 +132,19 @@ fun PostCell( } } } + + if (showNonUserDialog) { + CustomDialog( + title = "비회원 모드", + body = "회원만 게시물에 반응을 남길 수 있습니다. 로그인 화면으로 이동할까요?", + confirmText = "이동", + needsCancelButton = true, + onDismissRequest = { showNonUserDialog = false }, + dismiss = { showNonUserDialog = false }, + confirm = { + navController.navigateAsOrigin(NavigationDestination.Onboard) + } + ) + } } } \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/views/components/ReactionEmojiCell.kt b/android/app/src/main/java/com/goliath/emojihub/views/components/ReactionEmojiCell.kt new file mode 100644 index 00000000..3b287dcd --- /dev/null +++ b/android/app/src/main/java/com/goliath/emojihub/views/components/ReactionEmojiCell.kt @@ -0,0 +1,129 @@ +package com.goliath.emojihub.views.components + +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 +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.SaveAlt +import androidx.compose.material3.Divider +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import coil.compose.rememberAsyncImagePainter +import com.goliath.emojihub.extensions.toEmoji +import com.goliath.emojihub.models.Emoji +import com.goliath.emojihub.viewmodels.UserViewModel + + +@Composable +fun ReactionEmojiCell ( + emoji: Emoji, + reactedBy: String, + onSelected: (Emoji) -> Unit +) { + val thumbnailLink = emoji.thumbnailLink.takeIf{ it.isNotEmpty()} ?:"https://i.pinimg.com/236x/4b/05/0c/4b050ca4fcf588eedc58aa6135f5eecf.jpg" + + val userViewModel = hiltViewModel() + val currentUser = userViewModel.userState.collectAsState().value + + val textModifier = if (currentUser?.name == reactedBy) { + Modifier + .background(color = com.goliath.emojihub.ui.theme.Color.EmojiHubYellow, shape = RoundedCornerShape(10.dp)) + .padding(4.dp) + } else { + Modifier + } + + Column { + Card ( + modifier = Modifier + .fillMaxWidth() + .height(292.dp) + .clickable { onSelected(emoji) }, + shape = RoundedCornerShape(4.dp), + elevation = 0.dp + ) { + Box( + Modifier + .fillMaxSize() + .background(Color.Gray) + .alpha(0.25F)) + + Image( + painter = rememberAsyncImagePainter(thumbnailLink), + contentDescription = null, + modifier = Modifier.fillMaxWidth(), + contentScale = ContentScale.Crop + ) + + Box(modifier = Modifier.padding(8.dp)) { + Text( + text = "@" + emoji.createdBy, + modifier = Modifier.align(Alignment.TopStart), + fontSize = 12.sp, + color = com.goliath.emojihub.ui.theme.Color.White + ) + + Row( + modifier = Modifier.align(Alignment.BottomStart), + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + modifier = Modifier.size(16.dp), + onClick = {} + ) { + Icon( + imageVector = Icons.Filled.SaveAlt, + contentDescription = "", + tint = com.goliath.emojihub.ui.theme.Color.White + ) + } + + Text( + text = emoji.savedCount.toString(), + modifier = Modifier.padding(start = 4.dp), + fontSize = 12.sp, + color = com.goliath.emojihub.ui.theme.Color.White + ) + } + } + + Box(contentAlignment = Alignment.Center) { + Text( + text = emoji.unicode.toEmoji(), + fontSize = 44.sp + ) + } + } + if (currentUser != null) { + Text( + text = "@$reactedBy", + modifier = textModifier.align(Alignment.Start), + fontSize = 12.sp, + color = com.goliath.emojihub.ui.theme.Color.Black + ) + Spacer(modifier = Modifier.height(16.dp)) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/views/components/SaveEmojiListView.kt b/android/app/src/main/java/com/goliath/emojihub/views/components/SaveEmojiListView.kt deleted file mode 100644 index 7907dc1c..00000000 --- a/android/app/src/main/java/com/goliath/emojihub/views/components/SaveEmojiListView.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.goliath.emojihub.views.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.goliath.emojihub.LocalNavController -import com.goliath.emojihub.ui.theme.Color - -@Composable -fun SavedEmojiListView( - -) { - val navController = LocalNavController.current - - Column ( - Modifier.background(Color.White) - ) { - TopNavigationBar(navigate = { navController.popBackStack() }) - } -} \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/views/components/SavedEmojiListView.kt b/android/app/src/main/java/com/goliath/emojihub/views/components/SavedEmojiListView.kt new file mode 100644 index 00000000..abde7ec7 --- /dev/null +++ b/android/app/src/main/java/com/goliath/emojihub/views/components/SavedEmojiListView.kt @@ -0,0 +1,52 @@ +package com.goliath.emojihub.views.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.paging.compose.collectAsLazyPagingItems +import com.goliath.emojihub.LocalNavController +import com.goliath.emojihub.NavigationDestination +import com.goliath.emojihub.ui.theme.Color +import com.goliath.emojihub.viewmodels.EmojiViewModel + +@Composable +fun SavedEmojiListView( + emojiViewModel: EmojiViewModel +) { + val navController = LocalNavController.current + + val emojiList = emojiViewModel.mySavedEmojiList.collectAsLazyPagingItems() + + Column ( + Modifier.background(Color.White) + ) { + TopNavigationBar( + title = "저장된 이모지", + navigate = { navController.popBackStack() } + ) + + Column(Modifier.padding(horizontal = 16.dp)) { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier.padding(top = 18.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + items(emojiList.itemCount) { index -> + emojiList[index]?.let{ + EmojiCell(emoji = it, displayMode = EmojiCellDisplay.VERTICAL) { selectedEmoji -> + emojiViewModel.currentEmoji = selectedEmoji + navController.navigate(NavigationDestination.PlayEmojiVideo) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/goliath/emojihub/views/components/TopNavigationBar.kt b/android/app/src/main/java/com/goliath/emojihub/views/components/TopNavigationBar.kt index 63353cef..1b1a490e 100644 --- a/android/app/src/main/java/com/goliath/emojihub/views/components/TopNavigationBar.kt +++ b/android/app/src/main/java/com/goliath/emojihub/views/components/TopNavigationBar.kt @@ -28,13 +28,14 @@ fun TopNavigationBar( title: String = "", largeTitle: Boolean = true, shouldNavigate: Boolean = true, + needsElevation: Boolean = true, navigate: () -> Unit = {}, actions: @Composable () -> Unit = {}, ) { Surface( color = Color.Transparent, shape = RectangleShape, - elevation = 1.dp, + elevation = if (needsElevation) 1.dp else 0.dp, modifier = Modifier.height(64.dp), ) { Box(Modifier.padding(horizontal = 4.dp)) { diff --git a/android/app/src/test/java/com/goliath/emojihub/repositories/remote/EmojiRepositoryImplTest.kt b/android/app/src/test/java/com/goliath/emojihub/repositories/remote/EmojiRepositoryImplTest.kt index ddac0fc5..731a233a 100644 --- a/android/app/src/test/java/com/goliath/emojihub/repositories/remote/EmojiRepositoryImplTest.kt +++ b/android/app/src/test/java/com/goliath/emojihub/repositories/remote/EmojiRepositoryImplTest.kt @@ -3,6 +3,7 @@ package com.goliath.emojihub.repositories.remote import androidx.paging.testing.asSnapshot import com.goliath.emojihub.data_sources.CustomError import com.goliath.emojihub.data_sources.api.EmojiApi +import com.goliath.emojihub.data_sources.remote.EmojiDataSource import com.goliath.emojihub.mockLogClass import com.goliath.emojihub.models.EmojiDto import com.goliath.emojihub.models.UploadEmojiDto @@ -28,8 +29,8 @@ import java.lang.Exception @RunWith(JUnit4::class) class EmojiRepositoryImplTest { private val emojiApi = mockk() - private val context = mockk() - private val emojiRepositoryImpl = EmojiRepositoryImpl(emojiApi, context) + private val emojiDataSource = mockk() + private val emojiRepositoryImpl = EmojiRepositoryImpl(emojiApi, emojiDataSource) private val sampleEmojiDto = EmojiDto( createdBy = "channn", createdAt = "2023-11-24 14:25:05", @@ -56,7 +57,7 @@ class EmojiRepositoryImplTest { emojiApi.fetchEmojiList(any(), any(), any()) } returns Response.success(sampleEmojiDtoList) // when - val fetchedEmojiPagingDataFlow = runBlocking { emojiRepositoryImpl.fetchEmojiList() } + val fetchedEmojiPagingDataFlow = runBlocking { emojiRepositoryImpl.fetchEmojiList(1) } val fetchedEmojiDtoList = runBlocking { fetchedEmojiPagingDataFlow.asSnapshot() } // then coVerify(exactly = 2) { emojiApi.fetchEmojiList(any(), any(), any()) } @@ -123,22 +124,22 @@ class EmojiRepositoryImplTest { emojiApi.uploadEmoji(any(), any(), any()) } returns Response.success(Unit) - val emojiRepositoryImpl = spyk(EmojiRepositoryImpl(emojiApi, context)) + val emojiRepositoryImpl = spyk(EmojiRepositoryImpl(emojiApi, emojiDataSource)) every { - emojiRepositoryImpl.createVideoThumbnail(any(), any()) + emojiDataSource.createVideoThumbNail(any()) } returns File("sampleThumbnailFile") // when - val isUploaded = runBlocking { + val response = runBlocking { emojiRepositoryImpl.uploadEmoji(sampleVideoFile, sampleUploadEmojiDto) } // then coVerify(exactly = 1) { emojiApi.uploadEmoji(any(), any(), any()) } - assertTrue(isUploaded) + assertTrue(response.isSuccessful) } @Test - fun uploadEmoji_failureWithIOException_returnsFalse() { + fun uploadEmoji_failureWithException_throwsException() { // given mockkStatic(File::class) val sampleVideoFile = File("sampleVideoFile") @@ -147,18 +148,21 @@ class EmojiRepositoryImplTest { emojiApi.uploadEmoji(any(), any(), any()) } throws mockk() - val emojiRepositoryImpl = spyk(EmojiRepositoryImpl(emojiApi, context)) + val emojiRepositoryImpl = spyk(EmojiRepositoryImpl(emojiApi, emojiDataSource)) every { - emojiRepositoryImpl.createVideoThumbnail(any(), any()) + emojiDataSource.createVideoThumbNail(any()) } returns File("sampleThumbnailFile") // when - val isUploaded = runBlocking { - emojiRepositoryImpl.uploadEmoji(sampleVideoFile, sampleUploadEmojiDto) + try { + runBlocking { + emojiRepositoryImpl.uploadEmoji(sampleVideoFile, sampleUploadEmojiDto) + } + } catch (e: Exception) { + // then + coVerify(exactly = 1) { emojiApi.uploadEmoji(any(), any(), any()) } + assertTrue(e is IOException) } - // then - coVerify(exactly = 1) { emojiApi.uploadEmoji(any(), any(), any()) } - assertFalse(isUploaded) } @Test @@ -171,106 +175,63 @@ class EmojiRepositoryImplTest { emojiApi.uploadEmoji(any(), any(), any()) } throws mockk() - val emojiRepositoryImpl = spyk(EmojiRepositoryImpl(emojiApi, context)) + val emojiRepositoryImpl = spyk(EmojiRepositoryImpl(emojiApi, emojiDataSource)) every { - emojiRepositoryImpl.createVideoThumbnail(any(), any()) + emojiDataSource.createVideoThumbNail(any()) } returns File("sampleThumbnailFile") // when - val isUploaded = runBlocking { - emojiRepositoryImpl.uploadEmoji(sampleVideoFile, sampleUploadEmojiDto) + try { + runBlocking { + emojiRepositoryImpl.uploadEmoji(sampleVideoFile, sampleUploadEmojiDto) + } + } catch (e: Exception) { + // then + coVerify(exactly = 1) { emojiApi.uploadEmoji(any(), any(), any()) } + assertTrue(e is HttpException) } - // then - coVerify(exactly = 1) { emojiApi.uploadEmoji(any(), any(), any()) } - assertFalse(isUploaded) - } - - @Test - fun uploadEmoji_failureWithOtherException_returnsFalse() { - // given - mockkStatic(File::class) - val sampleVideoFile = File("sampleVideoFile") - val sampleUploadEmojiDto = mockk() - coEvery { - emojiApi.uploadEmoji(any(), any(), any()) - } throws spyk() - - val emojiRepositoryImpl = spyk(EmojiRepositoryImpl(emojiApi, context)) - every { - emojiRepositoryImpl.createVideoThumbnail(any(), any()) - } returns File("sampleThumbnailFile") - - // when - val isUploaded = runBlocking { - emojiRepositoryImpl.uploadEmoji(sampleVideoFile, sampleUploadEmojiDto) - } - // then - coVerify(exactly = 1) { emojiApi.uploadEmoji(any(), any(), any()) } - assertFalse(isUploaded) } @Test - fun saveEmoji_success_returnsSuccessResultUnit() { + fun saveEmoji_success_returnsSuccessResponseUnit() { // given val sampleEmojiId = "1234" coEvery { emojiApi.saveEmoji(any()) } returns Response.success(Unit) // when - val result = runBlocking { emojiRepositoryImpl.saveEmoji(sampleEmojiId) } + val response = runBlocking { emojiRepositoryImpl.saveEmoji(sampleEmojiId) } // then coVerify(exactly = 1) { emojiApi.saveEmoji(sampleEmojiId) } - assert(result.isSuccess) + assertTrue(response.isSuccessful) } @Test - fun saveEmoji_failure_returnsFailureResultUnit() { + fun saveEmoji_failure_returnsFailureResponseUnit() { // given val sampleEmojiId = "1234" coEvery { emojiApi.saveEmoji(any()) } returns Response.error(CustomError.BAD_REQUEST.statusCode, mockk(relaxed=true)) // when - val result = runBlocking { emojiRepositoryImpl.saveEmoji(sampleEmojiId) } - // then - coVerify(exactly = 1) { emojiApi.saveEmoji(sampleEmojiId) } - assertTrue(result.isFailure) - assertEquals( - "Failed to save Emoji (Id: $sampleEmojiId), 400", - result.exceptionOrNull()?.message - ) - } - - @Test - fun saveEmoji_exception_returnsFailureResultUnit() { - // given - val sampleEmojiId = "1234" - coEvery { - emojiApi.saveEmoji(any()) - } throws HttpException(mockk(relaxed=true)) - // when - val result = runBlocking { emojiRepositoryImpl.saveEmoji(sampleEmojiId) } + val response = runBlocking { emojiRepositoryImpl.saveEmoji(sampleEmojiId) } // then coVerify(exactly = 1) { emojiApi.saveEmoji(sampleEmojiId) } - assertTrue(result.isFailure) - assertEquals( - HttpException::class.java, - result.exceptionOrNull()?.javaClass - ) + assertFalse(response.isSuccessful) } @Test - fun unSaveEmoji_success_returnsSuccessResultUnit() { + fun unSaveEmoji_success_returnsSuccessResponseUnit() { // given val sampleEmojiId = "1234" coEvery { emojiApi.unSaveEmoji(any()) } returns Response.success(Unit) // when - val result = runBlocking { emojiRepositoryImpl.unSaveEmoji(sampleEmojiId) } + val response = runBlocking { emojiRepositoryImpl.unSaveEmoji(sampleEmojiId) } // then coVerify(exactly = 1) { emojiApi.unSaveEmoji(sampleEmojiId) } - assert(result.isSuccess) + assertTrue(response.isSuccessful) } @Test @@ -284,29 +245,7 @@ class EmojiRepositoryImplTest { val result = runBlocking { emojiRepositoryImpl.unSaveEmoji(sampleEmojiId) } // then coVerify(exactly = 1) { emojiApi.unSaveEmoji(sampleEmojiId) } - assertTrue(result.isFailure) - assertEquals( - "Failed to unsave Emoji (Id: $sampleEmojiId), 400", - result.exceptionOrNull()?.message - ) - } - - @Test - fun unSaveEmoji_exception_returnsFailureResponseUnit() { - // given - val sampleEmojiId = "1234" - coEvery { - emojiApi.unSaveEmoji(any()) - } throws HttpException(mockk(relaxed=true)) - // when - val result = runBlocking { emojiRepositoryImpl.unSaveEmoji(sampleEmojiId) } - // then - coVerify(exactly = 1) { emojiApi.unSaveEmoji(sampleEmojiId) } - assertTrue(result.isFailure) - assertEquals( - HttpException::class.java, - result.exceptionOrNull()?.javaClass - ) + assertFalse(result.isSuccessful) } // @Test diff --git a/android/app/src/test/java/com/goliath/emojihub/repositories/remote/UserRepositoryImplTest.kt b/android/app/src/test/java/com/goliath/emojihub/repositories/remote/UserRepositoryImplTest.kt index 877c979f..f09265aa 100644 --- a/android/app/src/test/java/com/goliath/emojihub/repositories/remote/UserRepositoryImplTest.kt +++ b/android/app/src/test/java/com/goliath/emojihub/repositories/remote/UserRepositoryImplTest.kt @@ -6,6 +6,7 @@ import com.goliath.emojihub.mockLogClass import com.goliath.emojihub.models.LoginUserDto import com.goliath.emojihub.models.RegisterUserDto import com.goliath.emojihub.models.responses.LoginResponseDto +import com.goliath.emojihub.sampleUserDetailsDto import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk @@ -36,6 +37,19 @@ class UserRepositoryImplTest { fun fetchUser() { } + @Test + fun fetchMyInfo_withValidToken_returnsResponseSuccessWithUserDetailsDto() { + // given + coEvery { + userApi.fetchMyInfo(any()) + } returns Response.success(sampleUserDetailsDto) + // when + val response = runBlocking { userRepositoryImpl.fetchMyInfo("sampleToken") } + // then + coVerify(exactly = 1) { userApi.fetchMyInfo("sampleToken") } + assertEquals(sampleUserDetailsDto, response.body()) + } + @Test fun registerUser_withValidRegisterUserDto_returnsResponseSuccessWithAccessToken() { // given @@ -114,4 +128,30 @@ class UserRepositoryImplTest { assertFalse(response.isSuccessful) assertEquals(401, response.code()) } + + @Test + fun logout_unit_returnsResponseSuccess() { + // given + coEvery { + userApi.logout() + } returns Response.success(Unit) + // when + val response = runBlocking { userRepositoryImpl.logout() } + // then + coVerify(exactly = 1) { userApi.logout() } + assertTrue(response.isSuccessful) + } + + @Test + fun signOut_withValidToken_returnsResponseSuccess() { + // given + coEvery { + userApi.signOut(any()) + } returns Response.success(Unit) + // when + val response = runBlocking { userRepositoryImpl.signOut("sampleToken") } + // then + coVerify(exactly = 1) { userApi.signOut("sampleToken") } + assertTrue(response.isSuccessful) + } } \ No newline at end of file diff --git a/android/app/src/test/java/com/goliath/emojihub/usecases/EmojiUseCaseImplTest.kt b/android/app/src/test/java/com/goliath/emojihub/usecases/EmojiUseCaseImplTest.kt index 0cc91866..c5aa66b3 100644 --- a/android/app/src/test/java/com/goliath/emojihub/usecases/EmojiUseCaseImplTest.kt +++ b/android/app/src/test/java/com/goliath/emojihub/usecases/EmojiUseCaseImplTest.kt @@ -3,7 +3,7 @@ package com.goliath.emojihub.usecases import androidx.paging.PagingData import androidx.paging.map import androidx.paging.testing.asSnapshot -import com.goliath.emojihub.createDeterministicDummyEmojiDtoList +import com.goliath.emojihub.createDeterministicTrendingEmojiDtoList import com.goliath.emojihub.data_sources.ApiErrorController import com.goliath.emojihub.mockLogClass import com.goliath.emojihub.models.CreatedEmoji @@ -24,6 +24,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 +import retrofit2.Response import java.io.File @RunWith(JUnit4::class) @@ -72,15 +73,15 @@ class EmojiUseCaseImplTest { @Test fun fetchEmojiList_returnsFlowOfEmojiPagingData() { // given - val sampleEmojiPagingDataFlow = createDeterministicDummyEmojiDtoList(5) + val sampleEmojiPagingDataFlow = createDeterministicTrendingEmojiDtoList(5) val sampleAnswer = sampleEmojiPagingDataFlow.map { it.map { dto -> Emoji(dto) } } coEvery { - emojiRepository.fetchEmojiList() + emojiRepository.fetchEmojiList(1) } returns sampleEmojiPagingDataFlow // when - val fetchedEmojiPagingDataFlow = runBlocking { emojiUseCaseImpl.fetchEmojiList() } + val fetchedEmojiPagingDataFlow = runBlocking { emojiUseCaseImpl.fetchEmojiList(1) } // then - coVerify(exactly = 1) { emojiRepository.fetchEmojiList() } + coVerify(exactly = 1) { emojiRepository.fetchEmojiList(1) } runBlocking { val sampleAnswerAsSnapshot = sampleAnswer.asSnapshot() val fetchedEmojiPagingDataFlowAsSnapshot = fetchedEmojiPagingDataFlow.asSnapshot() @@ -96,7 +97,7 @@ class EmojiUseCaseImplTest { @Test fun fetchMyCreatedEmojiList_returnsFlowOfEmojiPagingData() { // given - val sampleEmojiPagingDataFlow = createDeterministicDummyEmojiDtoList(5) + val sampleEmojiPagingDataFlow = createDeterministicTrendingEmojiDtoList(5) val sampleAnswer = sampleEmojiPagingDataFlow.map { it.map { dto -> Emoji(dto) } } coEvery { emojiRepository.fetchMyCreatedEmojiList() @@ -120,7 +121,7 @@ class EmojiUseCaseImplTest { @Test fun fetchMySavedEmojiList_returnsFlowOfEmojiPagingData() { // given - val sampleEmojiPagingDataFlow = createDeterministicDummyEmojiDtoList(5) + val sampleEmojiPagingDataFlow = createDeterministicTrendingEmojiDtoList(5) val sampleAnswer = sampleEmojiPagingDataFlow.map { it.map { dto -> Emoji(dto) } } coEvery { emojiRepository.fetchMySavedEmojiList() @@ -153,9 +154,7 @@ class EmojiUseCaseImplTest { x3dRepository.createEmoji(videoUri, topK) } returns sampleTop3CreatedEmojiList // when - val createdEmojiList = runBlocking { - emojiUseCaseImpl.createEmoji(videoUri, topK) - } + val createdEmojiList = runBlocking { emojiUseCaseImpl.createEmoji(videoUri, topK) } // then coVerify(exactly = 1) { x3dRepository.createEmoji(videoUri, topK) } assertEquals(sampleTop3CreatedEmojiList, createdEmojiList) @@ -170,9 +169,7 @@ class EmojiUseCaseImplTest { x3dRepository.createEmoji(videoUri, topK) } returns emptyList() // when - val createdEmojiList = runBlocking { - emojiUseCaseImpl.createEmoji(videoUri, topK) - } + val createdEmojiList = runBlocking { emojiUseCaseImpl.createEmoji(videoUri, topK) } // then coVerify(exactly = 1) { x3dRepository.createEmoji(videoUri, topK) } assertEquals(emptyList(), createdEmojiList) @@ -187,7 +184,7 @@ class EmojiUseCaseImplTest { val videoFile = File("sample.mp4") coEvery { emojiRepository.uploadEmoji(videoFile, any()) - } returns true + } returns Response.success(Unit) // when val isUploaded = runBlocking { emojiUseCaseImpl.uploadEmoji(emojiUnicode, emojiLabel, videoFile) @@ -208,14 +205,12 @@ class EmojiUseCaseImplTest { val sampleId = "sampleId" coEvery { emojiRepository.saveEmoji(sampleId) - } returns Result.success(Unit) + } returns Response.success(Unit) // when - val result = runBlocking { - emojiUseCaseImpl.saveEmoji(sampleId) - } + val isSuccess = runBlocking { emojiUseCaseImpl.saveEmoji(sampleId) } // then coVerify(exactly = 1) { emojiRepository.saveEmoji(sampleId) } - assertTrue(result.isSuccess) + assertTrue(isSuccess) } @Test @@ -224,14 +219,26 @@ class EmojiUseCaseImplTest { val sampleId = "sampleId" coEvery { emojiRepository.saveEmoji(sampleId) - } returns Result.failure(Exception("Failed to save Emoji (Id: $sampleId), 404")) + } returns Response.error(404, mockk(relaxed = true)) // when - val result = runBlocking { - emojiUseCaseImpl.saveEmoji(sampleId) - } + val isSuccess = runBlocking { emojiUseCaseImpl.saveEmoji(sampleId) } + // then + coVerify(exactly = 1) { emojiRepository.saveEmoji(sampleId) } + assertFalse(isSuccess) + } + + @Test + fun saveEmoji_exception_returnsFalse() { + // given + val sampleId = "sampleId" + coEvery { + emojiRepository.saveEmoji(sampleId) + } throws Exception("Failed to save Emoji (Id: $sampleId), 404") + // when + val isSuccess = runBlocking { emojiUseCaseImpl.saveEmoji(sampleId) } // then coVerify(exactly = 1) { emojiRepository.saveEmoji(sampleId) } - assertTrue(result.isFailure) + assertFalse(isSuccess) } @Test @@ -240,14 +247,12 @@ class EmojiUseCaseImplTest { val sampleId = "sampleId" coEvery { emojiRepository.unSaveEmoji(sampleId) - } returns Result.success(Unit) + } returns Response.success(Unit) // when - val result = runBlocking { - emojiUseCaseImpl.unSaveEmoji(sampleId) - } + val isSuccess = runBlocking { emojiUseCaseImpl.unSaveEmoji(sampleId) } // then coVerify(exactly = 1) { emojiRepository.unSaveEmoji(sampleId) } - assertTrue(result.isSuccess) + assertTrue(isSuccess) } @Test @@ -256,13 +261,25 @@ class EmojiUseCaseImplTest { val sampleId = "sampleId" coEvery { emojiRepository.unSaveEmoji(sampleId) - } returns Result.failure(Exception("Failed to unsave Emoji (Id: $sampleId), 404")) + } returns Response.error(404, mockk(relaxed = true)) // when - val result = runBlocking { - emojiUseCaseImpl.unSaveEmoji(sampleId) - } + val isSuccess = runBlocking { emojiUseCaseImpl.unSaveEmoji(sampleId) } + // then + coVerify(exactly = 1) { emojiRepository.unSaveEmoji(sampleId) } + assertFalse(isSuccess) + } + + @Test + fun unSaveEmoji_exception_returnsFalse() { + // given + val sampleId = "sampleId" + coEvery { + emojiRepository.unSaveEmoji(sampleId) + } throws Exception("Failed to unSave Emoji (Id: $sampleId), 404") + // when + val isSuccess = runBlocking { emojiUseCaseImpl.unSaveEmoji(sampleId) } // then coVerify(exactly = 1) { emojiRepository.unSaveEmoji(sampleId) } - assertTrue(result.isFailure) + assertFalse(isSuccess) } } \ No newline at end of file diff --git a/android/app/src/test/java/com/goliath/emojihub/usecases/UserUseCaseImplTest.kt b/android/app/src/test/java/com/goliath/emojihub/usecases/UserUseCaseImplTest.kt index d558ea77..376b7106 100644 --- a/android/app/src/test/java/com/goliath/emojihub/usecases/UserUseCaseImplTest.kt +++ b/android/app/src/test/java/com/goliath/emojihub/usecases/UserUseCaseImplTest.kt @@ -6,8 +6,10 @@ import com.goliath.emojihub.data_sources.CustomError import com.goliath.emojihub.data_sources.LocalStorage import com.goliath.emojihub.mockLogClass import com.goliath.emojihub.models.RegisterUserDto +import com.goliath.emojihub.models.UserDetails import com.goliath.emojihub.models.responses.LoginResponseDto import com.goliath.emojihub.repositories.remote.UserRepository +import com.goliath.emojihub.sampleUserDetailsDto import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -37,14 +39,14 @@ class UserUseCaseImplTest { // ! Fake SharedPreferences for testing class FakeSharedLocalStorage : LocalStorage { - private val fakePreference = mutableMapOf() + private val fakePreference = mutableMapOf() override var accessToken: String? get() = fakePreference.getOrDefault("accessToken", "") - set(value) = fakePreference.set("accessToken", value!!) + set(value) = fakePreference.set("accessToken", value) override var currentUser: String? get() = fakePreference.getOrDefault("currentUser", "") - set(value) = fakePreference.set("currentUser", value!!) + set(value) = fakePreference.set("currentUser", value) } @Before @@ -55,6 +57,39 @@ class UserUseCaseImplTest { userUseCaseImpl = UserUseCaseImpl(userRepository, apiErrorController) } + @Test + fun fetchMyInfo_success_updateUserDetailsState() { + // given + EmojiHubApplication.preferences.accessToken = sampleAccessToken + coEvery { + userRepository.fetchMyInfo(any()) + } returns Response.success(sampleUserDetailsDto) + // when + runBlocking { userUseCaseImpl.fetchMyInfo() } + // then + coVerify(exactly = 1) { userRepository.fetchMyInfo(any()) } + assertEquals( + UserDetails(sampleUserDetailsDto), + userUseCaseImpl.userDetailsState.value + ) + } + + @Test + fun fetchMyInfo_failure_updateErrorState() { + // given + EmojiHubApplication.preferences.accessToken = sampleAccessToken + coEvery { + userRepository.fetchMyInfo(any()) + } returns Response.error(CustomError.INTERNAL_SERVER_ERROR.statusCode, mockk(relaxed=true)) + // when + runBlocking { userUseCaseImpl.fetchMyInfo() } + // then + coVerify(exactly = 1) { userRepository.fetchMyInfo(any()) } + verify(exactly = 1) { + apiErrorController.setErrorState(CustomError.INTERNAL_SERVER_ERROR.statusCode) + } + } + @Test fun registerUser_withValidUserInfo_returnsTrue() { // given @@ -139,13 +174,73 @@ class UserUseCaseImplTest { verify(exactly = 1) { apiErrorController.setErrorState(401) } } - // @Test - // TODO: Not yet implemented - fun logout() { + @Test + fun logout_success_updateAccessTokenAndUserStateToNull() { + // given + coEvery { + userRepository.logout() + } returns Response.success(Unit) + // when + runBlocking { userUseCaseImpl.logout() } + // then + coVerify(exactly = 1) { userRepository.logout() } + assertEquals( + null, + userUseCaseImpl.accessTokenState.value + ) + assertEquals( + null, + userUseCaseImpl.userState.value + ) + } + + @Test + fun logout_failure_updateErrorState() { + // given + coEvery { + userRepository.logout() + } returns Response.error(CustomError.INTERNAL_SERVER_ERROR.statusCode, mockk(relaxed=true)) + // when + runBlocking { userUseCaseImpl.logout() } + // then + coVerify(exactly = 1) { userRepository.logout() } + verify(exactly = 1) { + apiErrorController.setErrorState(CustomError.INTERNAL_SERVER_ERROR.statusCode) + } + } + + @Test + fun signOut_success_updateAccessTokenAndUserStateToNull() { + // given + coEvery { + userRepository.signOut(any()) + } returns Response.success(Unit) + // when + runBlocking { userUseCaseImpl.signOut() } + // then + coVerify(exactly = 1) { userRepository.signOut(any()) } + assertEquals( + null, + userUseCaseImpl.accessTokenState.value + ) + assertEquals( + null, + userUseCaseImpl.userState.value + ) } - // @Test - // TODO: Not yet implemented - fun signOut() { + @Test + fun signOut_failure_updateErrorState() { + // given + coEvery { + userRepository.signOut(any()) + } returns Response.error(CustomError.INTERNAL_SERVER_ERROR.statusCode, mockk(relaxed=true)) + // when + runBlocking { userUseCaseImpl.signOut() } + // then + coVerify(exactly = 1) { userRepository.signOut(any()) } + verify(exactly = 1) { + apiErrorController.setErrorState(CustomError.INTERNAL_SERVER_ERROR.statusCode) + } } } \ No newline at end of file diff --git a/android/app/src/test/java/com/goliath/emojihub/utils.kt b/android/app/src/test/java/com/goliath/emojihub/utils.kt index 77048eb1..d579fa86 100644 --- a/android/app/src/test/java/com/goliath/emojihub/utils.kt +++ b/android/app/src/test/java/com/goliath/emojihub/utils.kt @@ -7,6 +7,8 @@ import com.goliath.emojihub.models.Emoji import com.goliath.emojihub.models.EmojiDto import com.goliath.emojihub.models.Post import com.goliath.emojihub.models.PostDto +import com.goliath.emojihub.models.ReactionWithEmojiUnicode +import com.goliath.emojihub.models.UserDetailsDto import com.goliath.emojihub.models.X3dInferenceResult import io.mockk.every import io.mockk.mockkStatic @@ -22,18 +24,28 @@ fun mockLogClass() { every { Log.e(any(), any()) } returns 0 } +// USER TESTING UTILS +val sampleUserDetailsDto = UserDetailsDto( + email = "sampleEmail", + name = "sampleName", + password = "samplePassword", + savedEmojiList = listOf("a", "b", "c"), + createdEmojiList = listOf("d", "e", "f"), + createdPostList = listOf("g", "h", "i"), +) + // EMOJI TESTING UTILS val dummyUsernames = listOf("channn", "doggydog", "meow_0w0", "mpunchmm", "kick_back") val dummyUnicodes = listOf("U+1F44D", "U+1F600", "U+1F970", "U+1F60E", "U+1F621", "U+1F63A", "U+1F496", "U+1F415") const val dummyMaxSavedCounts = 2000 -fun createDeterministicDummyEmojiDtoList(listSize : Int): Flow> { +fun createDeterministicTrendingEmojiDtoList(listSize : Int): Flow> { val dummyEmojiList = mutableListOf() for (i in 0 until listSize) { dummyEmojiList.add( EmojiDto( createdBy = dummyUsernames[i % dummyUsernames.size], - createdAt = "2023.09.16", - savedCount = dummyMaxSavedCounts % (i + 1), + createdAt = "2023."+i%12+".16", + savedCount = dummyMaxSavedCounts - i*10, videoLink = "", thumbnailLink = "", unicode = dummyUnicodes[i % dummyUnicodes.size], @@ -44,8 +56,8 @@ fun createDeterministicDummyEmojiDtoList(listSize : Int): Flow> { - return createDeterministicDummyEmojiDtoList(listSize).map { it.map { dto -> Emoji(dto) } } +fun createDeterministicTrendingEmojiList(listSize: Int): Flow> { + return createDeterministicTrendingEmojiDtoList(listSize).map { it.map { dto -> Emoji(dto) } } } // POST TESTING UTILS @@ -57,7 +69,10 @@ val samplePostDto = PostDto( "지갑이 하수구 구멍으로 빠지려는 찰나, 발로 굴러가는 지갑을 막아서 다행히 참사는 막을 수 있었다. " + "지갑 주인분께서 감사하다고 카페 드림에서 커피도 한 잔 사주셨다.", modifiedAt = "2023.10.23", - reaction = listOf("good", "check", "good") + reaction = listOf( + ReactionWithEmojiUnicode("3456", "U+1F44D"), + ReactionWithEmojiUnicode("5678", "U+1F44D") + ) ) fun createDeterministicDummyPostDtoList(listSize : Int): Flow> { val dummyPostList = mutableListOf() @@ -71,7 +86,10 @@ fun createDeterministicDummyPostDtoList(listSize : Int): Flow