From 19b61c959b6f5948e8aaac1f3504b84616f23a37 Mon Sep 17 00:00:00 2001 From: erik_everardo Date: Sat, 15 Jul 2023 20:58:57 -0600 Subject: [PATCH] WIP --- .idea/assetWizardSettings.xml | 6 +- .idea/kotlinc.xml | 6 + .idea/navEditor.xml | 242 ++++++++++++++++++ app/build.gradle | 14 + app/src/main/AndroidManifest.xml | 3 +- .../isolaatti/ActivityLifecycleCallbacks.kt | 43 ++++ .../main/java/com/isolaatti/MainActivity.kt | 19 +- .../main/java/com/isolaatti/MyApplication.kt | 10 + .../audio/recorder/ui/AudioDraftsFragment.kt | 6 + .../recorder/ui/AudioRecorderActivity.kt | 6 + .../recorder/ui/AudioRecorderMainFragment.kt | 6 + .../isolaatti/auth/data/AuthRepositoryImpl.kt | 45 +++- .../isolaatti/auth/data/local/TokenStorage.kt | 4 +- .../auth/data/remote/AuthTokenDto.kt | 2 +- .../isolaatti/auth/domain/AuthRepository.kt | 3 +- .../isolaatti/common/ErrorMessageViewModel.kt | 9 + .../isolaatti/common/IsolaattiBaseActivity.kt | 81 ++++++ .../isolaatti/connectivity/RetrofitClient.kt | 3 +- .../com/isolaatti/connectivity/SocketIO.kt | 26 ++ .../com/isolaatti/feed/data/remote/FeedApi.kt | 10 - .../com/isolaatti/feed/data/remote/FeedDto.kt | 6 - .../com/isolaatti/feed/data/remote/PostDto.kt | 12 - .../data/repository/FeedRepositoryImpl.kt | 20 -- .../com/isolaatti/feed/domain/model/Post.kt | 13 - .../feed/domain/repository/FeedRepository.kt | 8 - .../{feed/ui => home}/FeedFragment.kt | 25 +- .../java/com/isolaatti/home/HomeActivity.kt | 19 +- .../main/java/com/isolaatti/home/Module.kt | 27 -- .../search/presentation/SearchFragment.kt | 22 ++ .../java/com/isolaatti/login/LogInActivity.kt | 82 +++++- .../com/isolaatti/login/LogInViewModel.kt | 24 +- .../main/java/com/isolaatti/posting/Module.kt | 10 +- .../presentation/BottomSheetPostComments.kt | 2 +- .../posting/posts/data/remote/FeedDto.kt | 36 +++ .../posts/data/remote/FeedFilterDto.kt | 13 + .../data/remote/{PostsApi.kt => FeedsApi.kt} | 12 +- .../data/repository/PostsRepositoryImpl.kt | 54 +++- .../posting/posts/domain/PostsRepository.kt | 9 + .../presentation/PostsRecyclerViewAdapter.kt | 25 +- .../posts/presentation/PostsViewModel.kt | 35 ++- .../profile/data/remote/ProfileApi.kt | 1 - .../profile/presentation/ProfileViewModel.kt | 9 + .../isolaatti/profile/ui/ProfileActivity.kt | 27 +- .../main/java/com/isolaatti/utils/Resource.kt | 14 +- .../drawable/baseline_keyboard_voice_24.xml | 5 + app/src/main/res/drawable/baseline_mic_24.xml | 5 + .../res/drawable/baseline_post_add_24.xml | 9 + .../main/res/layout-land/fragment_feed.xml | 3 +- app/src/main/res/layout/activity_login.xml | 167 ++++++------ app/src/main/res/layout/activity_profile.xml | 144 +++++++---- .../res/layout/audio_recorder_activity.xml | 35 +++ .../layout/audio_recorder_main_fragment.xml | 6 + .../res/layout/bottom_sheet_post_comments.xml | 2 +- app/src/main/res/layout/fragment_feed.xml | 24 +- app/src/main/res/layout/fragment_images.xml | 29 ++- app/src/main/res/layout/fragment_search.xml | 34 +++ app/src/main/res/menu/home_topbar_menu.xml | 6 +- app/src/main/res/menu/new_menu.xml | 13 + .../navigation/audio_recorder_navigation.xml | 14 + .../main/res/navigation/home_navigation.xml | 22 +- app/src/main/res/values/strings.xml | 18 ++ build.gradle | 6 + 62 files changed, 1240 insertions(+), 351 deletions(-) create mode 100644 .idea/kotlinc.xml create mode 100644 .idea/navEditor.xml create mode 100644 app/src/main/java/com/isolaatti/ActivityLifecycleCallbacks.kt create mode 100644 app/src/main/java/com/isolaatti/audio/recorder/ui/AudioDraftsFragment.kt create mode 100644 app/src/main/java/com/isolaatti/audio/recorder/ui/AudioRecorderActivity.kt create mode 100644 app/src/main/java/com/isolaatti/audio/recorder/ui/AudioRecorderMainFragment.kt create mode 100644 app/src/main/java/com/isolaatti/common/ErrorMessageViewModel.kt create mode 100644 app/src/main/java/com/isolaatti/common/IsolaattiBaseActivity.kt create mode 100644 app/src/main/java/com/isolaatti/connectivity/SocketIO.kt delete mode 100644 app/src/main/java/com/isolaatti/feed/data/remote/FeedApi.kt delete mode 100644 app/src/main/java/com/isolaatti/feed/data/remote/FeedDto.kt delete mode 100644 app/src/main/java/com/isolaatti/feed/data/remote/PostDto.kt delete mode 100644 app/src/main/java/com/isolaatti/feed/data/repository/FeedRepositoryImpl.kt delete mode 100644 app/src/main/java/com/isolaatti/feed/domain/model/Post.kt delete mode 100644 app/src/main/java/com/isolaatti/feed/domain/repository/FeedRepository.kt rename app/src/main/java/com/isolaatti/{feed/ui => home}/FeedFragment.kt (84%) delete mode 100644 app/src/main/java/com/isolaatti/home/Module.kt create mode 100644 app/src/main/java/com/isolaatti/home/search/presentation/SearchFragment.kt create mode 100644 app/src/main/java/com/isolaatti/posting/posts/data/remote/FeedDto.kt create mode 100644 app/src/main/java/com/isolaatti/posting/posts/data/remote/FeedFilterDto.kt rename app/src/main/java/com/isolaatti/posting/posts/data/remote/{PostsApi.kt => FeedsApi.kt} (60%) create mode 100644 app/src/main/res/drawable/baseline_keyboard_voice_24.xml create mode 100644 app/src/main/res/drawable/baseline_mic_24.xml create mode 100644 app/src/main/res/drawable/baseline_post_add_24.xml create mode 100644 app/src/main/res/layout/audio_recorder_activity.xml create mode 100644 app/src/main/res/layout/audio_recorder_main_fragment.xml create mode 100644 app/src/main/res/layout/fragment_search.xml create mode 100644 app/src/main/res/menu/new_menu.xml create mode 100644 app/src/main/res/navigation/audio_recorder_navigation.xml diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml index d5a4cd7..4be5c4e 100644 --- a/.idea/assetWizardSettings.xml +++ b/.idea/assetWizardSettings.xml @@ -18,7 +18,7 @@ @@ -28,8 +28,8 @@ diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..2b8a50f --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/navEditor.xml b/.idea/navEditor.xml new file mode 100644 index 0000000..6d613d6 --- /dev/null +++ b/.idea/navEditor.xml @@ -0,0 +1,242 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index a3a53f3..24f1c73 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,7 @@ plugins { id 'kotlin-kapt' id 'com.google.dagger.hilt.android' id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.0' + id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' } android { @@ -89,6 +90,19 @@ dependencies { implementation "io.noties.markwon:editor:$markwon_version" implementation "io.noties.markwon:image-picasso:$markwon_version" implementation "io.noties.markwon:linkify:$markwon_version" + + + + implementation ('io.socket:socket.io-client:2.1.0') { + // excluding org.json which is provided by Android + exclude group: 'org.json', module: 'json' + } + + def room_version = "2.5.2" + + implementation "androidx.room:room-runtime:$room_version" + annotationProcessor "androidx.room:room-compiler:$room_version" + } kapt { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cf51f7f..398dcc5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,11 +11,12 @@ android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/Theme.Isolaatti" + android:usesCleartextTraffic="true" tools:targetApi="31"> + android:theme="@style/Theme.Isolaatti.Splash"> diff --git a/app/src/main/java/com/isolaatti/ActivityLifecycleCallbacks.kt b/app/src/main/java/com/isolaatti/ActivityLifecycleCallbacks.kt new file mode 100644 index 0000000..1a7204b --- /dev/null +++ b/app/src/main/java/com/isolaatti/ActivityLifecycleCallbacks.kt @@ -0,0 +1,43 @@ +package com.isolaatti + +import android.app.Activity +import android.app.Application +import android.os.Bundle +import com.isolaatti.connectivity.SocketIO + +class ActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks { + + var startedActivitiesCount = 0 + + override fun onActivityCreated(activity: Activity, bundle: Bundle?) { + startedActivitiesCount++ + } + + override fun onActivityStarted(activity: Activity) { + + } + + override fun onActivityResumed(activity: Activity) { + + } + + override fun onActivityPaused(activity: Activity) { + + } + + override fun onActivityStopped(activity: Activity) { + + } + + override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) { + + } + + override fun onActivityDestroyed(activity: Activity) { + startedActivitiesCount-- + + if(startedActivitiesCount == 0) { + SocketIO.disconnect() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/MainActivity.kt b/app/src/main/java/com/isolaatti/MainActivity.kt index b7b9c99..0fff242 100644 --- a/app/src/main/java/com/isolaatti/MainActivity.kt +++ b/app/src/main/java/com/isolaatti/MainActivity.kt @@ -1,8 +1,10 @@ package com.isolaatti +import android.app.Activity import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity +import androidx.activity.result.contract.ActivityResultContracts import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import com.isolaatti.auth.data.AuthRepositoryImpl import com.isolaatti.home.HomeActivity @@ -15,6 +17,14 @@ class MainActivity : ComponentActivity() { @Inject lateinit var authRepository: AuthRepositoryImpl + private val signInActivityResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> + if(activityResult.resultCode == Activity.RESULT_OK) { + val homeActivityIntent = Intent(this@MainActivity, HomeActivity::class.java) + homeActivityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(homeActivityIntent) + } + } + override fun onCreate(savedInstanceState: Bundle?) { var isLoading = true val splashScreen = installSplashScreen() @@ -25,11 +35,12 @@ class MainActivity : ComponentActivity() { val currentToken = authRepository.getCurrentToken() if(currentToken == null) { - startActivity(Intent(this@MainActivity, LogInActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NO_HISTORY - }) + + signInActivityResult.launch(Intent(this@MainActivity, LogInActivity::class.java)) } else { - startActivity(Intent(this@MainActivity, HomeActivity::class.java)) + val homeActivityIntent = Intent(this@MainActivity, HomeActivity::class.java) + homeActivityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(homeActivityIntent) } isLoading = false } diff --git a/app/src/main/java/com/isolaatti/MyApplication.kt b/app/src/main/java/com/isolaatti/MyApplication.kt index e9a0bcf..06b0365 100644 --- a/app/src/main/java/com/isolaatti/MyApplication.kt +++ b/app/src/main/java/com/isolaatti/MyApplication.kt @@ -1,6 +1,8 @@ package com.isolaatti +import android.app.Activity import android.app.Application +import android.os.Bundle import com.isolaatti.auth.data.AuthRepositoryImpl import com.isolaatti.auth.data.local.TokenStorage import com.isolaatti.auth.data.remote.AuthApi @@ -13,7 +15,15 @@ import javax.inject.Singleton @HiltAndroidApp class MyApplication : Application() { + private val activityLifecycleCallbacks = ActivityLifecycleCallbacks() + override fun onCreate() { super.onCreate() + registerActivityLifecycleCallbacks(activityLifecycleCallbacks) + } + + override fun onTerminate() { + super.onTerminate() + unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks) } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/audio/recorder/ui/AudioDraftsFragment.kt b/app/src/main/java/com/isolaatti/audio/recorder/ui/AudioDraftsFragment.kt new file mode 100644 index 0000000..28cbc01 --- /dev/null +++ b/app/src/main/java/com/isolaatti/audio/recorder/ui/AudioDraftsFragment.kt @@ -0,0 +1,6 @@ +package com.isolaatti.audio.recorder.ui + +import androidx.fragment.app.Fragment + +class AudioDraftsFragment : Fragment() { +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/audio/recorder/ui/AudioRecorderActivity.kt b/app/src/main/java/com/isolaatti/audio/recorder/ui/AudioRecorderActivity.kt new file mode 100644 index 0000000..f7b1085 --- /dev/null +++ b/app/src/main/java/com/isolaatti/audio/recorder/ui/AudioRecorderActivity.kt @@ -0,0 +1,6 @@ +package com.isolaatti.audio.recorder.ui + +import androidx.activity.ComponentActivity + +class AudioRecorderActivity : ComponentActivity() { +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/audio/recorder/ui/AudioRecorderMainFragment.kt b/app/src/main/java/com/isolaatti/audio/recorder/ui/AudioRecorderMainFragment.kt new file mode 100644 index 0000000..c46b94a --- /dev/null +++ b/app/src/main/java/com/isolaatti/audio/recorder/ui/AudioRecorderMainFragment.kt @@ -0,0 +1,6 @@ +package com.isolaatti.audio.recorder.ui + +import androidx.fragment.app.Fragment + +class AudioRecorderMainFragment : Fragment() { +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/auth/data/AuthRepositoryImpl.kt b/app/src/main/java/com/isolaatti/auth/data/AuthRepositoryImpl.kt index 1c04631..8994698 100644 --- a/app/src/main/java/com/isolaatti/auth/data/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/isolaatti/auth/data/AuthRepositoryImpl.kt @@ -1,18 +1,16 @@ package com.isolaatti.auth.data -import android.util.Log import com.isolaatti.auth.data.remote.AuthTokenDto import com.isolaatti.auth.data.local.TokenStorage import com.isolaatti.auth.data.remote.AuthApi import com.isolaatti.auth.data.remote.Credential import com.isolaatti.auth.domain.AuthRepository -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Dispatchers +import com.isolaatti.utils.Resource import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.launch +import retrofit2.await import retrofit2.awaitResponse +import java.io.IOException import javax.inject.Inject @@ -20,22 +18,41 @@ class AuthRepositoryImpl @Inject constructor( private val tokenStorage: TokenStorage, private val authApi: AuthApi ) : AuthRepository { - override fun authWithEmailAndPassword(email: String, password: String): Flow = flow { - val res = authApi.signInWithEmailAndPassword(Credential(email, password)).awaitResponse() - val authDto = res.body() + override fun authWithEmailAndPassword( + email: String, + password: String + ): Flow> = flow { + try { + val res = + authApi.signInWithEmailAndPassword(Credential(email, password)).awaitResponse() + if(res.isSuccessful) { + val dto = res.body() + if(dto == null) { + emit(Resource.Error(Resource.Error.ErrorType.ServerError)) + return@flow + } + tokenStorage.storeToken(dto) + emit(Resource.Success(true)) + return@flow + } - if(authDto != null) { - tokenStorage.storeToken(authDto) - emit(true) - } else { - emit(false) + when(res.code()){ + 401 -> emit(Resource.Error(Resource.Error.ErrorType.AuthError)) + 404 -> emit(Resource.Error(Resource.Error.ErrorType.NotFoundError)) + 500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError)) + else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError)) + } + } catch (_: Exception) { + emit(Resource.Error(Resource.Error.ErrorType.NetworkError)) } } override fun logout(): Flow = flow { tokenStorage.removeToken() - authApi.signOut() + try { + authApi.signOut().awaitResponse() + } catch(_: Exception) { } emit(true) } diff --git a/app/src/main/java/com/isolaatti/auth/data/local/TokenStorage.kt b/app/src/main/java/com/isolaatti/auth/data/local/TokenStorage.kt index 9d77841..d381bd7 100644 --- a/app/src/main/java/com/isolaatti/auth/data/local/TokenStorage.kt +++ b/app/src/main/java/com/isolaatti/auth/data/local/TokenStorage.kt @@ -79,7 +79,9 @@ class TokenStorage @Inject constructor(@ApplicationContext private val applicati fun removeToken() { if(file.exists()) { - file.delete() + try { + file.delete() + } catch(_: SecurityException) { } } } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/auth/data/remote/AuthTokenDto.kt b/app/src/main/java/com/isolaatti/auth/data/remote/AuthTokenDto.kt index e979f6d..af44141 100644 --- a/app/src/main/java/com/isolaatti/auth/data/remote/AuthTokenDto.kt +++ b/app/src/main/java/com/isolaatti/auth/data/remote/AuthTokenDto.kt @@ -1,6 +1,6 @@ package com.isolaatti.auth.data.remote -data class AuthTokenDto(val expires: String, val created: String, val token: String) { +data class AuthTokenDto(val token: String) { override fun toString(): String { return token } diff --git a/app/src/main/java/com/isolaatti/auth/domain/AuthRepository.kt b/app/src/main/java/com/isolaatti/auth/domain/AuthRepository.kt index c8b521b..4edd198 100644 --- a/app/src/main/java/com/isolaatti/auth/domain/AuthRepository.kt +++ b/app/src/main/java/com/isolaatti/auth/domain/AuthRepository.kt @@ -1,10 +1,11 @@ package com.isolaatti.auth.domain import com.isolaatti.auth.data.remote.AuthTokenDto +import com.isolaatti.utils.Resource import kotlinx.coroutines.flow.Flow interface AuthRepository { - fun authWithEmailAndPassword(email: String, password: String): Flow + fun authWithEmailAndPassword(email: String, password: String): Flow> fun logout(): Flow fun getCurrentToken(): AuthTokenDto? } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/common/ErrorMessageViewModel.kt b/app/src/main/java/com/isolaatti/common/ErrorMessageViewModel.kt new file mode 100644 index 0000000..13653c2 --- /dev/null +++ b/app/src/main/java/com/isolaatti/common/ErrorMessageViewModel.kt @@ -0,0 +1,9 @@ +package com.isolaatti.common + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.isolaatti.utils.Resource + +class ErrorMessageViewModel : ViewModel() { + val error: MutableLiveData = MutableLiveData() +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/common/IsolaattiBaseActivity.kt b/app/src/main/java/com/isolaatti/common/IsolaattiBaseActivity.kt new file mode 100644 index 0000000..dc3d4a0 --- /dev/null +++ b/app/src/main/java/com/isolaatti/common/IsolaattiBaseActivity.kt @@ -0,0 +1,81 @@ +package com.isolaatti.common + +import android.app.Activity +import android.content.DialogInterface +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Observer +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.isolaatti.R +import com.isolaatti.home.HomeActivity +import com.isolaatti.utils.Resource +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +abstract class IsolaattiBaseActivity : AppCompatActivity() { + + val errorViewModel: ErrorMessageViewModel by viewModels() + + private val errorObserver: Observer = Observer { + when(it) { + Resource.Error.ErrorType.AuthError -> showReAuthDialog() + Resource.Error.ErrorType.NetworkError -> showNetworkErrorMessage() + Resource.Error.ErrorType.NotFoundError -> showNotFoundErrorMessage() + Resource.Error.ErrorType.ServerError -> showServerErrorMessage() + Resource.Error.ErrorType.OtherError -> showUnknownErrorMessage() + else -> {} + } + } + + private val signInActivityResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> + if(activityResult.resultCode == Activity.RESULT_OK) { + onRetry() + } + } + + /** + * This method is called when a refresh should be performed. For example, + * when sign in flow is started completed from here, it is needed to know + * when it is complete. + */ + abstract fun onRetry() + + private val onAcceptReAuthClick = DialogInterface.OnClickListener { _, _ -> + + } + + private fun showReAuthDialog() { + MaterialAlertDialogBuilder(this) + .setMessage(R.string.need_reauth_message) + .setPositiveButton(R.string.accept, onAcceptReAuthClick) + .setNegativeButton(R.string.close, null) + .show() + } + + private fun showNetworkErrorMessage() { + Toast.makeText(this, R.string.network_error, Toast.LENGTH_SHORT).show() + } + + private fun showServerErrorMessage() { + + } + + private fun showNotFoundErrorMessage() { + + } + + private fun showUnknownErrorMessage() { + + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + errorViewModel.error.observe(this, errorObserver) + Log.d("IsolaattiBaseActivity", errorViewModel.toString()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/connectivity/RetrofitClient.kt b/app/src/main/java/com/isolaatti/connectivity/RetrofitClient.kt index 0f806c6..ba0b91b 100644 --- a/app/src/main/java/com/isolaatti/connectivity/RetrofitClient.kt +++ b/app/src/main/java/com/isolaatti/connectivity/RetrofitClient.kt @@ -1,5 +1,6 @@ package com.isolaatti.connectivity +import com.isolaatti.BuildConfig import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory @@ -14,7 +15,7 @@ class RetrofitClient @Inject constructor(private val authenticationInterceptor: val excludedUrlsFromAuthentication = listOf( "/api/LogIn" ) - const val BASE_URL = "https://isolaatti.com/api/" + const val BASE_URL = "${BuildConfig.backend}/api/" } private val okHttpClient get() = OkHttpClient.Builder() diff --git a/app/src/main/java/com/isolaatti/connectivity/SocketIO.kt b/app/src/main/java/com/isolaatti/connectivity/SocketIO.kt new file mode 100644 index 0000000..bf1699f --- /dev/null +++ b/app/src/main/java/com/isolaatti/connectivity/SocketIO.kt @@ -0,0 +1,26 @@ +package com.isolaatti.connectivity + +import io.socket.client.IO +import io.socket.client.Socket + +object SocketIO { + private lateinit var socket: Socket + val instance: Socket get() { + return if(this::socket.isInitialized) { + if(socket.connected()) { + socket + } else { + IO.socket("") + } + } else { + socket = IO.socket("") + socket + } + } + + fun disconnect() { + if(this::socket.isInitialized) { + socket.disconnect() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/feed/data/remote/FeedApi.kt b/app/src/main/java/com/isolaatti/feed/data/remote/FeedApi.kt deleted file mode 100644 index dd8a6a6..0000000 --- a/app/src/main/java/com/isolaatti/feed/data/remote/FeedApi.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.isolaatti.feed.data.remote - -import retrofit2.Call -import retrofit2.http.GET -import retrofit2.http.Query - -interface FeedApi { - @GET("Feed") - fun fetchFeed(@Query("lastId") lastId: Long, @Query("length") length: Int): Call -} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/feed/data/remote/FeedDto.kt b/app/src/main/java/com/isolaatti/feed/data/remote/FeedDto.kt deleted file mode 100644 index b971171..0000000 --- a/app/src/main/java/com/isolaatti/feed/data/remote/FeedDto.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.isolaatti.feed.data.remote - -data class FeedDto( - val data: MutableList, - val moreContent: Boolean -) \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/feed/data/remote/PostDto.kt b/app/src/main/java/com/isolaatti/feed/data/remote/PostDto.kt deleted file mode 100644 index 3db1d40..0000000 --- a/app/src/main/java/com/isolaatti/feed/data/remote/PostDto.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.isolaatti.feed.data.remote - -import com.isolaatti.feed.domain.model.Post - -data class PostDto( - val post: Post, - var numberOfLikes: Int, - var numberOfComments: Int, - val userName: String, - val squadName: String?, - var liked: Boolean -) \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/feed/data/repository/FeedRepositoryImpl.kt b/app/src/main/java/com/isolaatti/feed/data/repository/FeedRepositoryImpl.kt deleted file mode 100644 index 2f5cb5e..0000000 --- a/app/src/main/java/com/isolaatti/feed/data/repository/FeedRepositoryImpl.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.isolaatti.feed.data.repository - -import com.isolaatti.feed.data.remote.FeedApi -import com.isolaatti.feed.data.remote.FeedDto -import com.isolaatti.feed.domain.repository.FeedRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import retrofit2.awaitResponse -import javax.inject.Inject - -class FeedRepositoryImpl @Inject constructor(private val feedApi: FeedApi): FeedRepository { - override fun getNextPage(lastId: Long, count: Int): Flow = flow { - val response = feedApi.fetchFeed(lastId, count).awaitResponse().body() - if(response != null){ - emit(response) - } else { - emit(null) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/feed/domain/model/Post.kt b/app/src/main/java/com/isolaatti/feed/domain/model/Post.kt deleted file mode 100644 index 5c79842..0000000 --- a/app/src/main/java/com/isolaatti/feed/domain/model/Post.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.isolaatti.feed.domain.model - -data class Post( - val id: Long, - var textContent: String, - val userId: Int, - val privacy: Int, - val date: String, - var audioId: String, - val squadId: String, - val linkedDiscussionId: Long, - val linkedCommentId: Long -) \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/feed/domain/repository/FeedRepository.kt b/app/src/main/java/com/isolaatti/feed/domain/repository/FeedRepository.kt deleted file mode 100644 index 9a4db92..0000000 --- a/app/src/main/java/com/isolaatti/feed/domain/repository/FeedRepository.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.isolaatti.feed.domain.repository - -import com.isolaatti.feed.data.remote.FeedDto -import kotlinx.coroutines.flow.Flow - -interface FeedRepository { - fun getNextPage(lastId: Long, count: Int): Flow -} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/feed/ui/FeedFragment.kt b/app/src/main/java/com/isolaatti/home/FeedFragment.kt similarity index 84% rename from app/src/main/java/com/isolaatti/feed/ui/FeedFragment.kt rename to app/src/main/java/com/isolaatti/home/FeedFragment.kt index d82a3eb..34650e2 100644 --- a/app/src/main/java/com/isolaatti/feed/ui/FeedFragment.kt +++ b/app/src/main/java/com/isolaatti/home/FeedFragment.kt @@ -1,4 +1,4 @@ -package com.isolaatti.feed.ui +package com.isolaatti.home import android.content.Intent import android.os.Bundle @@ -10,6 +10,7 @@ import android.view.ViewGroup import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.LinearLayoutManager import com.isolaatti.R +import com.isolaatti.common.ErrorMessageViewModel import com.isolaatti.databinding.FragmentFeedBinding import com.isolaatti.posting.posts.presentation.PostsViewModel import com.isolaatti.posting.comments.presentation.BottomSheetPostComments @@ -34,6 +35,7 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback { } private val viewModel: PostsViewModel by activityViewModels() + private val errorViewModel: ErrorMessageViewModel by activityViewModels() private lateinit var viewBinding: FragmentFeedBinding private lateinit var adapter: PostsRecyclerViewAdapter @@ -67,6 +69,7 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback { } } + val markwon = Markwon.builder(requireContext()) .usePlugin(object: AbstractMarkwonPlugin() { override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { @@ -78,21 +81,33 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback { .usePlugin(PicassoImagesPluginDef.picassoImagePlugin) .usePlugin(LinkifyPlugin.create()) .build() - adapter = PostsRecyclerViewAdapter(markwon, this, listOf()) + adapter = PostsRecyclerViewAdapter(markwon, this, null) viewBinding.feedRecyclerView.adapter = adapter viewBinding.feedRecyclerView.layoutManager = LinearLayoutManager(requireContext()) viewModel.posts.observe(viewLifecycleOwner){ Log.d("recycler", it.data.toString()) - adapter.updateList(it.data.toList(),null) + adapter.updateList(it,null) + } + Log.d("FeedFragment", errorViewModel.toString()) + viewModel.errorLoading.observe(viewLifecycleOwner) { + errorViewModel.error.postValue(it) + Log.d("FeedFragment", it.toString()) } viewModel.postLiked.observe(viewLifecycleOwner) { - adapter.updateList(viewModel.posts.value?.data, PostsRecyclerViewAdapter.UpdateEvent( - PostsRecyclerViewAdapter.UpdateEvent.UpdateType.POST_LIKED, it.postId)) + viewModel.posts.value?.let { feed -> + adapter.updateList( + feed, PostsRecyclerViewAdapter.UpdateEvent( + PostsRecyclerViewAdapter.UpdateEvent.UpdateType.POST_LIKED, it.postId)) + } } } + fun onNewMenuItemClicked(v: View) { + + } + override fun onLiked(postId: Long) = viewModel.likePost(postId) override fun onUnLiked(postId: Long) = viewModel.unLikePost(postId) diff --git a/app/src/main/java/com/isolaatti/home/HomeActivity.kt b/app/src/main/java/com/isolaatti/home/HomeActivity.kt index 52d394a..82a2b39 100644 --- a/app/src/main/java/com/isolaatti/home/HomeActivity.kt +++ b/app/src/main/java/com/isolaatti/home/HomeActivity.kt @@ -4,17 +4,24 @@ import android.os.Bundle import android.view.Menu import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Observer import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController import com.isolaatti.R +import com.isolaatti.common.ErrorMessageViewModel +import com.isolaatti.common.IsolaattiBaseActivity import com.isolaatti.databinding.ActivityHomeBinding import com.isolaatti.posting.posts.presentation.PostsViewModel +import com.isolaatti.utils.Resource import dagger.hilt.android.AndroidEntryPoint -@AndroidEntryPoint -class HomeActivity : AppCompatActivity() { - lateinit var viewBinding: ActivityHomeBinding - val postsViewModel: PostsViewModel by viewModels() +class HomeActivity : IsolaattiBaseActivity() { + private lateinit var viewBinding: ActivityHomeBinding + private val postsViewModel: PostsViewModel by viewModels() + override fun onRetry() { + + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -33,4 +40,8 @@ class HomeActivity : AppCompatActivity() { override fun onCreateOptionsMenu(menu: Menu?): Boolean { return super.onCreateOptionsMenu(menu) } + + override fun onDestroy() { + super.onDestroy() + } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/home/Module.kt b/app/src/main/java/com/isolaatti/home/Module.kt deleted file mode 100644 index e1655c3..0000000 --- a/app/src/main/java/com/isolaatti/home/Module.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.isolaatti.home - -import com.isolaatti.connectivity.RetrofitClient -import com.isolaatti.feed.data.remote.FeedApi -import com.isolaatti.feed.data.repository.FeedRepositoryImpl -import com.isolaatti.feed.domain.repository.FeedRepository -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent -import dagger.hilt.android.scopes.ActivityScoped -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -class Module { - @Provides - fun provideFeedApi(retrofitClient: RetrofitClient): FeedApi { - return retrofitClient.client.create(FeedApi::class.java) - } - - @Provides - fun provideFeedRepository(feedApi: FeedApi): FeedRepository { - return FeedRepositoryImpl(feedApi) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/home/search/presentation/SearchFragment.kt b/app/src/main/java/com/isolaatti/home/search/presentation/SearchFragment.kt new file mode 100644 index 0000000..64be041 --- /dev/null +++ b/app/src/main/java/com/isolaatti/home/search/presentation/SearchFragment.kt @@ -0,0 +1,22 @@ +package com.isolaatti.home.search.presentation + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.isolaatti.databinding.FragmentSearchBinding + +class SearchFragment : Fragment() { + + lateinit var viewBinding: FragmentSearchBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + viewBinding = FragmentSearchBinding.inflate(inflater) + return viewBinding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/login/LogInActivity.kt b/app/src/main/java/com/isolaatti/login/LogInActivity.kt index e77b748..391a6a2 100644 --- a/app/src/main/java/com/isolaatti/login/LogInActivity.kt +++ b/app/src/main/java/com/isolaatti/login/LogInActivity.kt @@ -1,20 +1,31 @@ package com.isolaatti.login +import android.app.Activity import android.content.Intent +import android.net.Uri import android.os.Bundle +import android.view.View import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.browser.customtabs.CustomTabsIntent import androidx.core.widget.doOnTextChanged import androidx.lifecycle.ViewModelProvider +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.isolaatti.BuildConfig +import com.isolaatti.R +import com.isolaatti.common.IsolaattiBaseActivity import com.isolaatti.databinding.ActivityLoginBinding import com.isolaatti.home.HomeActivity +import com.isolaatti.utils.Resource import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class LogInActivity: AppCompatActivity() { + + lateinit var viewBinding: ActivityLoginBinding - val viewModel: LogInViewModel by viewModels() + private val viewModel: LogInViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -23,11 +34,33 @@ class LogInActivity: AppCompatActivity() { setContentView(viewBinding.root) viewModel.signInSuccess.observe(this) {success -> - if(success) - startActivity(Intent(this, HomeActivity::class.java)) - else - Toast.makeText(this,"Could not sign in, your credential is not correct...", Toast.LENGTH_SHORT).show() - // Show login error message. + if(success) { + setResult(Activity.RESULT_OK) + finish() + } + + } + + viewModel.signInLoading.observe(this) { + viewBinding.progressBar.visibility = View.VISIBLE + viewBinding.signInBtn.isEnabled = false + viewBinding.textFieldEmail.isEnabled = false + viewBinding.textFieldPassword.isEnabled = false + } + viewModel.signInError.observe(this) { + viewBinding.progressBar.visibility = View.GONE + viewBinding.signInBtn.isEnabled = true + viewBinding.textFieldEmail.isEnabled = true + viewBinding.textFieldPassword.isEnabled = true + when(it) { + Resource.Error.ErrorType.NetworkError -> showNetworkErrorMessage() + Resource.Error.ErrorType.AuthError -> showWrongPasswordErrorMessage() + Resource.Error.ErrorType.NotFoundError -> showNotFoundErrorMessage() + Resource.Error.ErrorType.ServerError -> showServerErrorMessage() + Resource.Error.ErrorType.OtherError -> showUnknownErrorMessage() + null -> {} + } + } viewModel.formIsValid.observe(this) {isValid -> @@ -50,5 +83,42 @@ class LogInActivity: AppCompatActivity() { viewModel.signIn(email.toString(), password.toString()) } + viewBinding.forgotPasswordBtn.setOnClickListener { + openForgotPassword() + } + + } + + private fun openForgotPassword() { + CustomTabsIntent.Builder() + .setShowTitle(true) + .build() + .launchUrl(this, Uri.parse("${BuildConfig.backend}/recuperacion_cuenta")) + } + + private fun showWrongPasswordErrorMessage() { + MaterialAlertDialogBuilder(this) + .setMessage(R.string.wrong_password) + .setNeutralButton(R.string.forgot_password) {_,_ -> openForgotPassword()} + .setPositiveButton(R.string.dismiss, null) + .show() + } + private fun showNetworkErrorMessage() { + Toast.makeText(this, R.string.network_error, Toast.LENGTH_SHORT).show() + } + + private fun showServerErrorMessage() { + + } + + private fun showNotFoundErrorMessage() { + MaterialAlertDialogBuilder(this) + .setMessage(getString(R.string.account_not_found, viewBinding.textFieldEmail.editText?.text)) + .setPositiveButton(R.string.dismiss, null) + .show() + } + + private fun showUnknownErrorMessage() { + } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/login/LogInViewModel.kt b/app/src/main/java/com/isolaatti/login/LogInViewModel.kt index 17a6751..bd9d862 100644 --- a/app/src/main/java/com/isolaatti/login/LogInViewModel.kt +++ b/app/src/main/java/com/isolaatti/login/LogInViewModel.kt @@ -5,16 +5,22 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.isolaatti.auth.domain.AuthRepository +import com.isolaatti.utils.Resource import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class LogInViewModel @Inject constructor(private val authRepository: AuthRepository): ViewModel() { val signInSuccess: MutableLiveData = MutableLiveData() + val signInError: MutableLiveData = MutableLiveData() + val signInLoading: MutableLiveData = MutableLiveData() val formIsValid: MutableLiveData = MutableLiveData(false) val emailUserInputIsValid: MutableLiveData = MutableLiveData(false) val passwordUserInputIsValid: MutableLiveData = MutableLiveData(false) @@ -33,11 +39,23 @@ class LogInViewModel @Inject constructor(private val authRepository: AuthReposit } fun signIn(email: String, password: String) { + signInError.postValue(null) viewModelScope.launch { - authRepository.authWithEmailAndPassword(email, password).collect { + authRepository.authWithEmailAndPassword(email, password).onEach { Log.d("login", it.toString()) - signInSuccess.postValue(it) - } + when(it) { + is Resource.Success -> { + signInLoading.postValue(false) + signInSuccess.postValue(true) + } + is Resource.Error -> { + signInLoading.postValue(false) + signInError.postValue(it.errorType) + } + is Resource.Loading -> signInLoading.postValue(true) + } + + }.flowOn(Dispatchers.IO).launchIn(this) } } diff --git a/app/src/main/java/com/isolaatti/posting/Module.kt b/app/src/main/java/com/isolaatti/posting/Module.kt index 771cac5..a09a669 100644 --- a/app/src/main/java/com/isolaatti/posting/Module.kt +++ b/app/src/main/java/com/isolaatti/posting/Module.kt @@ -1,7 +1,7 @@ package com.isolaatti.posting import com.isolaatti.connectivity.RetrofitClient -import com.isolaatti.posting.posts.data.remote.PostsApi +import com.isolaatti.posting.posts.data.remote.FeedsApi import com.isolaatti.posting.posts.data.repository.PostsRepositoryImpl import com.isolaatti.posting.posts.domain.PostsRepository import dagger.Module @@ -13,12 +13,12 @@ import dagger.hilt.components.SingletonComponent @InstallIn(SingletonComponent::class) class Module { @Provides - fun providePostsApi(retrofitClient: RetrofitClient): PostsApi { - return retrofitClient.client.create(PostsApi::class.java) + fun providePostsApi(retrofitClient: RetrofitClient): FeedsApi { + return retrofitClient.client.create(FeedsApi::class.java) } @Provides - fun providePostsRepository(postsApi: PostsApi): PostsRepository { - return PostsRepositoryImpl(postsApi) + fun providePostsRepository(feedsApi: FeedsApi): PostsRepository { + return PostsRepositoryImpl(feedsApi) } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/posting/comments/presentation/BottomSheetPostComments.kt b/app/src/main/java/com/isolaatti/posting/comments/presentation/BottomSheetPostComments.kt index e594b68..3967f3e 100644 --- a/app/src/main/java/com/isolaatti/posting/comments/presentation/BottomSheetPostComments.kt +++ b/app/src/main/java/com/isolaatti/posting/comments/presentation/BottomSheetPostComments.kt @@ -94,7 +94,7 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC val textField = viewBinding.newCommentTextField textField.setStartIconOnClickListener { - AlertDialog.Builder(requireContext()).setView(R.layout.write_comment_multiline_dialog).show() + } optionsViewModel.optionClicked(-1) diff --git a/app/src/main/java/com/isolaatti/posting/posts/data/remote/FeedDto.kt b/app/src/main/java/com/isolaatti/posting/posts/data/remote/FeedDto.kt new file mode 100644 index 0000000..59749e1 --- /dev/null +++ b/app/src/main/java/com/isolaatti/posting/posts/data/remote/FeedDto.kt @@ -0,0 +1,36 @@ +package com.isolaatti.posting.posts.data.remote + +data class FeedDto( + val data: MutableList, + var moreContent: Boolean +) { + + fun concatFeed(otherFeed: FeedDto?): FeedDto { + if(otherFeed != null) { + data.addAll(otherFeed.data) + moreContent = otherFeed.moreContent + } + + return this + } + data class PostDto( + val post: Post, + var numberOfLikes: Int, + var numberOfComments: Int, + val userName: String, + val squadName: String?, + var liked: Boolean + ) { + data class Post( + val id: Long, + var textContent: String, + val userId: Int, + val privacy: Int, + val date: String, + var audioId: String, + val squadId: String, + val linkedDiscussionId: Long, + val linkedCommentId: Long + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/posting/posts/data/remote/FeedFilterDto.kt b/app/src/main/java/com/isolaatti/posting/posts/data/remote/FeedFilterDto.kt new file mode 100644 index 0000000..d1408ca --- /dev/null +++ b/app/src/main/java/com/isolaatti/posting/posts/data/remote/FeedFilterDto.kt @@ -0,0 +1,13 @@ +package com.isolaatti.posting.posts.data.remote + +data class FeedFilterDto( + val includeAudio: String, + val includeFromSquads: String, + val dataRange: DataRange +) { + data class DataRange( + val enabled: Boolean, + val from: String, + val to: String + ) +} diff --git a/app/src/main/java/com/isolaatti/posting/posts/data/remote/PostsApi.kt b/app/src/main/java/com/isolaatti/posting/posts/data/remote/FeedsApi.kt similarity index 60% rename from app/src/main/java/com/isolaatti/posting/posts/data/remote/PostsApi.kt rename to app/src/main/java/com/isolaatti/posting/posts/data/remote/FeedsApi.kt index ba12acb..6ab7fae 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/data/remote/PostsApi.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/data/remote/FeedsApi.kt @@ -1,23 +1,25 @@ package com.isolaatti.posting.posts.data.remote -import com.isolaatti.feed.data.remote.FeedDto -import com.isolaatti.feed.data.remote.PostDto import com.isolaatti.profile.data.remote.ProfileListItemDto import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query -interface PostsApi { +interface FeedsApi { @GET("Fetch/PostsOfUser/{userId}") fun postsOfUser(@Path("userId") userId: Int, @Query("length") length: Int, @Query("lastId") lastId: Long, - @Query("olderFirst") olderFirst: Boolean): Call + @Query("olderFirst") olderFirst: Boolean, + @Query(value = "filterJson", encoded = false) filter: String): Call @GET("Fetch/Post/{postId}") - fun getPost(@Path("postId") postId: Long): Call + fun getPost(@Path("postId") postId: Long): Call @GET("Fetch/Post/{postId}/LikedBy") fun getLikedBy(@Path("postId") postId: Long): Call> + + @GET("Feed") + fun getChronology(@Query("lastId") lastId: Long, @Query("length") length: Int): Call } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/posting/posts/data/repository/PostsRepositoryImpl.kt b/app/src/main/java/com/isolaatti/posting/posts/data/repository/PostsRepositoryImpl.kt index fe23043..35eaf99 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/data/repository/PostsRepositoryImpl.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/data/repository/PostsRepositoryImpl.kt @@ -1,8 +1,58 @@ package com.isolaatti.posting.posts.data.repository -import com.isolaatti.posting.posts.data.remote.PostsApi +import com.google.gson.Gson +import com.isolaatti.posting.posts.data.remote.FeedDto +import com.isolaatti.posting.posts.data.remote.FeedFilterDto +import com.isolaatti.posting.posts.data.remote.FeedsApi import com.isolaatti.posting.posts.domain.PostsRepository +import com.isolaatti.utils.Resource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.flow +import retrofit2.await +import java.io.IOException +import java.lang.RuntimeException import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine -class PostsRepositoryImpl @Inject constructor(private val postsApi: PostsApi) : PostsRepository { +class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi) : PostsRepository { + override fun getFeed(lastId: Long): Flow> = flow { + emit(Resource.Loading()) + try { + val result = feedsApi.getChronology(lastId, 20).execute() + if(result.isSuccessful) { + emit(Resource.Success(result.body())) + return@flow + } + when(result.code()) { + 401 -> emit(Resource.Error(Resource.Error.ErrorType.AuthError)) + 404 -> emit(Resource.Error(Resource.Error.ErrorType.NotFoundError)) + 500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError)) + else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError)) + } + } catch(_: Exception) { + emit(Resource.Error(Resource.Error.ErrorType.NetworkError)) + } + } + + override fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto): Flow> = flow { + emit(Resource.Loading()) + try { + val gson = Gson() + val result = feedsApi.postsOfUser(userId, 20, lastId, olderFirst, gson.toJson(filter)).execute() + if(result.isSuccessful) { + emit(Resource.Success(result.body())) + return@flow + } + when(result.code()) { + 401 -> emit(Resource.Error(Resource.Error.ErrorType.AuthError)) + 404 -> emit(Resource.Error(Resource.Error.ErrorType.NotFoundError)) + 500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError)) + else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError)) + } + } catch(_: Exception) { + emit(Resource.Error(Resource.Error.ErrorType.NetworkError)) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/posting/posts/domain/PostsRepository.kt b/app/src/main/java/com/isolaatti/posting/posts/domain/PostsRepository.kt index 22ff079..68c92cf 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/domain/PostsRepository.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/domain/PostsRepository.kt @@ -1,4 +1,13 @@ package com.isolaatti.posting.posts.domain +import com.isolaatti.posting.posts.data.remote.FeedDto +import com.isolaatti.posting.posts.data.remote.FeedFilterDto +import com.isolaatti.utils.Resource +import kotlinx.coroutines.flow.Flow + interface PostsRepository { + + fun getFeed(lastId: Long): Flow> + + fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto): Flow> } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/posting/posts/presentation/PostsRecyclerViewAdapter.kt b/app/src/main/java/com/isolaatti/posting/posts/presentation/PostsRecyclerViewAdapter.kt index c5c656b..0391062 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/presentation/PostsRecyclerViewAdapter.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/presentation/PostsRecyclerViewAdapter.kt @@ -11,15 +11,15 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.google.android.material.button.MaterialButton import com.isolaatti.R -import com.isolaatti.feed.data.remote.PostDto +import com.isolaatti.posting.posts.data.remote.FeedDto import com.isolaatti.posting.common.domain.OnUserInteractedWithPostCallback import com.isolaatti.utils.UrlGen.userProfileImage import com.squareup.picasso.Picasso import io.noties.markwon.Markwon -class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callback: OnUserInteractedWithPostCallback, private var list: List) : RecyclerView.Adapter(){ +class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callback: OnUserInteractedWithPostCallback, private var feedDto: FeedDto?) : RecyclerView.Adapter(){ inner class FeedViewHolder(itemView: View) : ViewHolder(itemView) { - fun bindView(postDto: PostDto, payloads: List) { + fun bindView(postDto: FeedDto.PostDto, payloads: List) { Log.d("payloads", payloads.count().toString()) val likeButton: MaterialButton = itemView.findViewById(R.id.like_button) @@ -120,29 +120,24 @@ class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callba return FeedViewHolder(view) } - override fun getItemCount(): Int = list.size + override fun getItemCount(): Int = feedDto?.data?.size ?: 0 - override fun getItemId(position: Int): Long = list[position].post.id override fun setHasStableIds(hasStableIds: Boolean) { super.setHasStableIds(true) } @SuppressLint("NotifyDataSetChanged") - fun updateList(newList: List?, updateEvent: UpdateEvent? = null) { + fun updateList(updatedFeed: FeedDto, updateEvent: UpdateEvent? = null) { if(updateEvent == null) { - if(newList != null) { - list = newList - } + feedDto = updatedFeed notifyDataSetChanged() return } - val postUpdated = list.find { p -> p.post.id == updateEvent.affectedId } ?: return - val position = list.indexOf(postUpdated) + val postUpdated = feedDto?.data?.find { p -> p.post.id == updateEvent.affectedId } ?: return + val position = feedDto?.data?.indexOf(postUpdated) ?: return - if(newList != null) { - list = newList - } + feedDto = updatedFeed when(updateEvent.updateType) { UpdateEvent.UpdateType.POST_LIKED -> { @@ -163,6 +158,6 @@ class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callba override fun onBindViewHolder(holder: FeedViewHolder, position: Int) {} override fun onBindViewHolder(holder: FeedViewHolder, position: Int, payloads: List) { - holder.bindView(list[position], payloads) + holder.bindView(feedDto?.data?.get(position) ?: return, payloads) } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/posting/posts/presentation/PostsViewModel.kt b/app/src/main/java/com/isolaatti/posting/posts/presentation/PostsViewModel.kt index d095ee2..846aa16 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/presentation/PostsViewModel.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/presentation/PostsViewModel.kt @@ -6,11 +6,14 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.isolaatti.posting.comments.data.remote.FeedCommentsDto -import com.isolaatti.feed.data.remote.FeedDto -import com.isolaatti.feed.domain.repository.FeedRepository +import com.isolaatti.posting.posts.data.remote.FeedDto import com.isolaatti.posting.likes.data.remote.LikeDto import com.isolaatti.posting.likes.domain.repository.LikesRepository +import com.isolaatti.posting.posts.data.repository.PostsRepositoryImpl +import com.isolaatti.posting.posts.domain.PostsRepository +import com.isolaatti.utils.Resource import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn @@ -19,11 +22,17 @@ import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class PostsViewModel @Inject constructor(private val feedRepository: FeedRepository, private val likesRepository: LikesRepository) : ViewModel() { +class PostsViewModel @Inject constructor(private val postsRepository: PostsRepository, private val likesRepository: LikesRepository) : ViewModel() { private val _posts: MutableLiveData = MutableLiveData() val posts: LiveData get() = _posts + private val _loadingPosts = MutableLiveData(false) + val loadingPosts: LiveData get() = _loadingPosts + + private val _errorLoading: MutableLiveData = MutableLiveData() + val errorLoading: LiveData get() = _errorLoading + private val _comments: MutableLiveData = MutableLiveData() val comments: LiveData get() = _comments @@ -34,19 +43,19 @@ class PostsViewModel @Inject constructor(private val feedRepository: FeedReposit fun getFeed() { viewModelScope.launch { - feedRepository.getNextPage(getLastId(), 20).onEach {feedDto -> - val temp = _posts.value - if(temp != null) { - feedDto?.data?.let { it -> - temp.data.addAll(it) + postsRepository.getFeed(getLastId()).onEach { + when(it) { + is Resource.Success -> { + _loadingPosts.postValue(false) + _posts.postValue(posts.value?.concatFeed(it.data) ?: it.data) } - temp.let { - _posts.postValue(it) + is Resource.Loading -> { + _loadingPosts.postValue(true) + } + is Resource.Error -> { + _errorLoading.postValue(it.errorType) } - } else { - _posts.postValue(feedDto) } - }.flowOn(Dispatchers.IO).launchIn(this) } } diff --git a/app/src/main/java/com/isolaatti/profile/data/remote/ProfileApi.kt b/app/src/main/java/com/isolaatti/profile/data/remote/ProfileApi.kt index 7b6787e..155fed7 100644 --- a/app/src/main/java/com/isolaatti/profile/data/remote/ProfileApi.kt +++ b/app/src/main/java/com/isolaatti/profile/data/remote/ProfileApi.kt @@ -1,6 +1,5 @@ package com.isolaatti.profile.data.remote -import com.isolaatti.feed.data.remote.FeedDto import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Path diff --git a/app/src/main/java/com/isolaatti/profile/presentation/ProfileViewModel.kt b/app/src/main/java/com/isolaatti/profile/presentation/ProfileViewModel.kt index 5c25afa..b626f2e 100644 --- a/app/src/main/java/com/isolaatti/profile/presentation/ProfileViewModel.kt +++ b/app/src/main/java/com/isolaatti/profile/presentation/ProfileViewModel.kt @@ -1,10 +1,19 @@ package com.isolaatti.profile.presentation +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import com.isolaatti.profile.data.remote.UserProfileDto import com.isolaatti.profile.domain.ProfileRepository import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel class ProfileViewModel @Inject constructor(private val profileRepository: ProfileRepository) : ViewModel() { + private val _profile = MutableLiveData() + val profile: LiveData get() = _profile + + fun getProfile(profileId: Int) { + + } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/profile/ui/ProfileActivity.kt b/app/src/main/java/com/isolaatti/profile/ui/ProfileActivity.kt index 1312a80..0ace859 100644 --- a/app/src/main/java/com/isolaatti/profile/ui/ProfileActivity.kt +++ b/app/src/main/java/com/isolaatti/profile/ui/ProfileActivity.kt @@ -2,15 +2,21 @@ package com.isolaatti.profile.ui import android.os.Bundle import androidx.activity.addCallback +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider import androidx.viewpager.widget.PagerAdapter import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentViewHolder import com.google.android.material.tabs.TabLayoutMediator import com.isolaatti.R import com.isolaatti.databinding.ActivityProfileBinding +import com.isolaatti.profile.data.remote.UserProfileDto +import com.isolaatti.profile.presentation.ProfileViewModel +import com.isolaatti.utils.UrlGen import com.squareup.picasso.Picasso import dagger.hilt.android.AndroidEntryPoint @@ -38,19 +44,24 @@ class ProfileActivity : AppCompatActivity() { lateinit var viewBinding: ActivityProfileBinding + private val viewModel: ProfileViewModel by viewModels() + private val profileObserver = Observer { profile -> + Picasso.get() + .load(UrlGen.userProfileImage(profile.id)) + .into(viewBinding.profileImageView) + + viewBinding.textViewUsername.text = profile.name + viewBinding.textViewDescription.text = profile.descriptionText + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewBinding = ActivityProfileBinding.inflate(layoutInflater) - setContentView(viewBinding.root) - Picasso.get().load("https://isolaatti.com/api/images/image/63a2a6c5270ecc2be2512799?mode=reduced").into(viewBinding.profileImageView) - viewBinding.textViewUsername.text = "Erik Everardo" - viewBinding.textViewDescription.text = "Hola" viewBinding.profileViewPager2.adapter = ViewPagerAdapter(this) viewBinding.topAppBar.setNavigationOnClickListener { - onBackPressed() + finish() } TabLayoutMediator(viewBinding.profileTabLayout, viewBinding.profileViewPager2) {tab, position -> @@ -66,5 +77,11 @@ class ProfileActivity : AppCompatActivity() { } } }.attach() + + viewModel.profile.observe(this, profileObserver) + } + + companion object { + const val EXTRA_USER_ID = "user_id" } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/utils/Resource.kt b/app/src/main/java/com/isolaatti/utils/Resource.kt index 142e46d..5842b25 100644 --- a/app/src/main/java/com/isolaatti/utils/Resource.kt +++ b/app/src/main/java/com/isolaatti/utils/Resource.kt @@ -1,9 +1,11 @@ package com.isolaatti.utils -class Resource { - inner class Success(data: T) - - inner class Loading() - - inner class Error +abstract class Resource { + class Success(val data: T?): Resource() + class Loading: Resource() + class Error(val errorType: ErrorType? = null): Resource() { + enum class ErrorType { + NetworkError, AuthError, NotFoundError, ServerError, OtherError + } + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_keyboard_voice_24.xml b/app/src/main/res/drawable/baseline_keyboard_voice_24.xml new file mode 100644 index 0000000..70bb82a --- /dev/null +++ b/app/src/main/res/drawable/baseline_keyboard_voice_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_mic_24.xml b/app/src/main/res/drawable/baseline_mic_24.xml new file mode 100644 index 0000000..5eb92eb --- /dev/null +++ b/app/src/main/res/drawable/baseline_mic_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_post_add_24.xml b/app/src/main/res/drawable/baseline_post_add_24.xml new file mode 100644 index 0000000..141a0a9 --- /dev/null +++ b/app/src/main/res/drawable/baseline_post_add_24.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/app/src/main/res/layout-land/fragment_feed.xml b/app/src/main/res/layout-land/fragment_feed.xml index d4032bb..63aae5c 100644 --- a/app/src/main/res/layout-land/fragment_feed.xml +++ b/app/src/main/res/layout-land/fragment_feed.xml @@ -17,7 +17,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/home_drawer" app:layout_constraintTop_toTopOf="parent" - tools:context=".feed.ui.FeedFragment"> + tools:context=".home.FeedFragment"> @@ -48,6 +48,7 @@ android:layout_height="match_parent" app:title="@string/app_name" app:titleCentered="true" + app:menu="@menu/new_menu" tools:layout_conversion_absoluteHeight="64dp" tools:layout_conversion_absoluteWidth="531dp" tools:layout_editor_absoluteX="360dp" diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index e62d3eb..1b8587d 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -1,92 +1,105 @@ - - + + + + - - + + android:layout_height="wrap_content"> - - - - - + android:textAlignment="center" + android:textAppearance="?attr/textAppearanceHeadlineLarge" + android:layout_margin="24dp" + android:text="@string/app_name"/> - - - - - + android:textAppearance="?attr/textAppearanceHeadlineSmall" + android:layout_margin="24dp" + android:text="@string/sign_in"/> - + -