creacion de post, se ve informacion de perfil en drawer y otros cambios

This commit is contained in:
Erik Cavazos 2023-07-27 22:49:55 -06:00
parent 1e6be6db5b
commit 72290eb50f
40 changed files with 749 additions and 53 deletions

View File

@ -27,6 +27,7 @@
<activity android:name=".login.LogInActivity" android:theme="@style/Theme.Isolaatti" /> <activity android:name=".login.LogInActivity" android:theme="@style/Theme.Isolaatti" />
<activity android:name=".profile.ui.ProfileActivity" android:theme="@style/Theme.Isolaatti"/> <activity android:name=".profile.ui.ProfileActivity" android:theme="@style/Theme.Isolaatti"/>
<activity android:name=".settings.ui.SettingsActivity" android:theme="@style/Theme.Isolaatti"/> <activity android:name=".settings.ui.SettingsActivity" android:theme="@style/Theme.Isolaatti"/>
<activity android:name=".posting.posts.ui.CreatePostActivity" android:theme="@style/Theme.Isolaatti" android:windowSoftInputMode="adjustResize"/>
</application> </application>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
</manifest> </manifest>

View File

@ -25,5 +25,4 @@ class MainModule {
return RetrofitClient(authenticationInterceptor) return RetrofitClient(authenticationInterceptor)
} }
} }

View File

@ -1,13 +1,17 @@
package com.isolaatti.auth package com.isolaatti.auth
import android.content.Context
import com.isolaatti.auth.data.AuthRepositoryImpl import com.isolaatti.auth.data.AuthRepositoryImpl
import com.isolaatti.auth.data.local.KeyValueDao
import com.isolaatti.auth.data.local.TokenStorage import com.isolaatti.auth.data.local.TokenStorage
import com.isolaatti.auth.data.remote.AuthApi import com.isolaatti.auth.data.remote.AuthApi
import com.isolaatti.auth.domain.AuthRepository import com.isolaatti.auth.domain.AuthRepository
import com.isolaatti.connectivity.RetrofitClient import com.isolaatti.connectivity.RetrofitClient
import com.isolaatti.database.AppDatabase
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
@Module @Module
@ -18,7 +22,12 @@ class Module {
return retrofitClient.client.create(AuthApi::class.java) return retrofitClient.client.create(AuthApi::class.java)
} }
@Provides @Provides
fun provideAuthRepository(tokenStorage: TokenStorage, authApi: AuthApi): AuthRepository { fun provideKeyValueDao(database: AppDatabase): KeyValueDao {
return AuthRepositoryImpl(tokenStorage, authApi) return database.keyValueDao()
}
@Provides
fun provideAuthRepository(tokenStorage: TokenStorage, authApi: AuthApi, keyValueDao: KeyValueDao): AuthRepository {
return AuthRepositoryImpl(tokenStorage, authApi, keyValueDao)
} }
} }

View File

@ -1,5 +1,7 @@
package com.isolaatti.auth.data package com.isolaatti.auth.data
import com.isolaatti.auth.data.local.KeyValueDao
import com.isolaatti.auth.data.local.KeyValueEntity
import com.isolaatti.auth.data.remote.AuthTokenDto import com.isolaatti.auth.data.remote.AuthTokenDto
import com.isolaatti.auth.data.local.TokenStorage import com.isolaatti.auth.data.local.TokenStorage
import com.isolaatti.auth.data.remote.AuthApi import com.isolaatti.auth.data.remote.AuthApi
@ -8,16 +10,18 @@ import com.isolaatti.auth.domain.AuthRepository
import com.isolaatti.utils.Resource import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import retrofit2.await
import retrofit2.awaitResponse import retrofit2.awaitResponse
import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
class AuthRepositoryImpl @Inject constructor( class AuthRepositoryImpl @Inject constructor(
private val tokenStorage: TokenStorage, private val tokenStorage: TokenStorage,
private val authApi: AuthApi private val authApi: AuthApi,
private val keyValueDao: KeyValueDao
) : AuthRepository { ) : AuthRepository {
companion object {
val KEY_USERID = "user_id"
}
override fun authWithEmailAndPassword( override fun authWithEmailAndPassword(
email: String, email: String,
password: String password: String
@ -33,6 +37,7 @@ class AuthRepositoryImpl @Inject constructor(
return@flow return@flow
} }
tokenStorage.storeToken(dto) tokenStorage.storeToken(dto)
keyValueDao.setValue(KeyValueEntity(KEY_USERID, dto.userId.toString()))
emit(Resource.Success(true)) emit(Resource.Success(true))
return@flow return@flow
} }
@ -60,4 +65,8 @@ class AuthRepositoryImpl @Inject constructor(
return tokenStorage.token return tokenStorage.token
} }
override fun getUserId(): Flow<Int?> = flow {
emit(keyValueDao.getValue(KEY_USERID).toIntOrNull())
}
} }

View File

@ -0,0 +1,15 @@
package com.isolaatti.auth.data.local
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface KeyValueDao {
@Query("SELECT value FROM key_values WHERE id = :key")
fun getValue(key: String): String
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun setValue(entity: KeyValueEntity)
}

View File

@ -0,0 +1,10 @@
package com.isolaatti.auth.data.local
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "key_values")
data class KeyValueEntity(
@PrimaryKey val id: String,
val value: String
)

View File

@ -1,6 +1,6 @@
package com.isolaatti.auth.data.remote package com.isolaatti.auth.data.remote
data class AuthTokenDto(val token: String) { data class AuthTokenDto(val token: String, val userId: Int) {
override fun toString(): String { override fun toString(): String {
return token return token
} }

View File

@ -8,4 +8,6 @@ interface AuthRepository {
fun authWithEmailAndPassword(email: String, password: String): Flow<Resource<Boolean>> fun authWithEmailAndPassword(email: String, password: String): Flow<Resource<Boolean>>
fun logout(): Flow<Boolean> fun logout(): Flow<Boolean>
fun getCurrentToken(): AuthTokenDto? fun getCurrentToken(): AuthTokenDto?
fun getUserId(): Flow<Int?>
} }

View File

@ -13,6 +13,7 @@ import androidx.lifecycle.Observer
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.home.HomeActivity import com.isolaatti.home.HomeActivity
import com.isolaatti.login.LogInActivity
import com.isolaatti.utils.Resource import com.isolaatti.utils.Resource
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -46,7 +47,7 @@ abstract class IsolaattiBaseActivity : AppCompatActivity() {
abstract fun onRetry() abstract fun onRetry()
private val onAcceptReAuthClick = DialogInterface.OnClickListener { _, _ -> private val onAcceptReAuthClick = DialogInterface.OnClickListener { _, _ ->
signInActivityResult.launch(Intent(this, LogInActivity::class.java))
} }
private fun showReAuthDialog() { private fun showReAuthDialog() {

View File

@ -0,0 +1,12 @@
package com.isolaatti.database
import androidx.room.Database
import androidx.room.RoomDatabase
import com.isolaatti.auth.data.local.KeyValueDao
import com.isolaatti.auth.data.local.KeyValueEntity
@Database(entities = [KeyValueEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun keyValueDao(): KeyValueDao
}

View File

@ -0,0 +1,20 @@
package com.isolaatti.database
import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class Module {
@Provides
@Singleton
fun provideAppDatabase(@ApplicationContext applicationContext: Context): AppDatabase {
return Room.databaseBuilder(applicationContext, AppDatabase::class.java, "database.db").build()
}
}

View File

@ -1,5 +1,6 @@
package com.isolaatti.home package com.isolaatti.home
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
@ -7,20 +8,30 @@ import androidx.fragment.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.isolaatti.BuildConfig import com.isolaatti.BuildConfig
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.common.ErrorMessageViewModel import com.isolaatti.common.ErrorMessageViewModel
import com.isolaatti.databinding.FragmentFeedBinding import com.isolaatti.databinding.FragmentFeedBinding
import com.isolaatti.home.presentation.FeedViewModel
import com.isolaatti.posting.posts.presentation.PostsViewModel import com.isolaatti.posting.posts.presentation.PostsViewModel
import com.isolaatti.posting.comments.presentation.BottomSheetPostComments import com.isolaatti.posting.comments.presentation.BottomSheetPostComments
import com.isolaatti.posting.common.domain.OnUserInteractedWithPostCallback import com.isolaatti.posting.common.domain.OnUserInteractedWithPostCallback
import com.isolaatti.posting.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment import com.isolaatti.posting.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment
import com.isolaatti.posting.posts.presentation.PostsRecyclerViewAdapter import com.isolaatti.posting.posts.presentation.PostsRecyclerViewAdapter
import com.isolaatti.posting.posts.ui.CreatePostActivity
import com.isolaatti.profile.ui.ProfileActivity import com.isolaatti.profile.ui.ProfileActivity
import com.isolaatti.settings.ui.SettingsActivity import com.isolaatti.settings.ui.SettingsActivity
import com.isolaatti.utils.PicassoImagesPluginDef import com.isolaatti.utils.PicassoImagesPluginDef
import com.isolaatti.utils.UrlGen
import com.squareup.picasso.Picasso
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.noties.markwon.AbstractMarkwonPlugin import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
@ -37,9 +48,16 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
private val viewModel: PostsViewModel by activityViewModels() private val viewModel: PostsViewModel by activityViewModels()
private val errorViewModel: ErrorMessageViewModel by activityViewModels() private val errorViewModel: ErrorMessageViewModel by activityViewModels()
private val screenViewModel: FeedViewModel by viewModels()
private lateinit var viewBinding: FragmentFeedBinding private lateinit var viewBinding: FragmentFeedBinding
private lateinit var adapter: PostsRecyclerViewAdapter private lateinit var adapter: PostsRecyclerViewAdapter
private val createDiscussion = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if(it.resultCode == Activity.RESULT_OK) {
Toast.makeText(requireContext(), R.string.posted_successfully, Toast.LENGTH_SHORT).show()
}
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
@ -99,6 +117,33 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
viewModel.getFeed(refresh = false) viewModel.getFeed(refresh = false)
} }
viewBinding.topAppBar.setOnMenuItemClickListener {
when(it.itemId) {
R.id.menu_item_new_discussion -> {
createDiscussion.launch(Intent(requireContext(),CreatePostActivity::class.java))
true
}
else -> {
false
}
}
}
screenViewModel.userProfile.observe(viewLifecycleOwner) {
val header = viewBinding.homeDrawer.getHeaderView(0) as? ConstraintLayout
val image: ImageView? = header?.findViewById(R.id.profileImageView)
val textViewName: TextView? = header?.findViewById(R.id.textViewName)
val textViewEmail: TextView? = header?.findViewById(R.id.textViewEmail)
Picasso.get().load(UrlGen.userProfileImage(it.id)).into(image)
textViewName?.text = it.name
textViewEmail?.text = it.email
}
viewModel.posts.observe(viewLifecycleOwner){ viewModel.posts.observe(viewLifecycleOwner){
if (it != null) { if (it != null) {
@ -128,6 +173,7 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
PostsRecyclerViewAdapter.UpdateEvent.UpdateType.POST_LIKED, it.postId)) PostsRecyclerViewAdapter.UpdateEvent.UpdateType.POST_LIKED, it.postId))
} }
} }
screenViewModel.getProfile()
} }
fun onNewMenuItemClicked(v: View) { fun onNewMenuItemClicked(v: View) {

View File

@ -0,0 +1,42 @@
package com.isolaatti.home.presentation
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.isolaatti.auth.domain.AuthRepository
import com.isolaatti.profile.data.remote.UserProfileDto
import com.isolaatti.profile.domain.use_case.GetProfile
import com.isolaatti.utils.Resource
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
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 FeedViewModel @Inject constructor(private val getProfileUseCase: GetProfile, private val authRepository: AuthRepository) : ViewModel() {
// User profile
private val _userProfile: MutableLiveData<UserProfileDto> = MutableLiveData()
val userProfile: LiveData<UserProfileDto> get() = _userProfile
fun getProfile() {
viewModelScope.launch {
authRepository.getUserId().onEach {userId ->
userId?.let {
getProfileUseCase(userId).onEach {profile ->
if(profile is Resource.Success) {
profile.data?.let { _userProfile.postValue(it) }
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
}

View File

@ -2,12 +2,14 @@ package com.isolaatti.posting
import com.isolaatti.connectivity.RetrofitClient import com.isolaatti.connectivity.RetrofitClient
import com.isolaatti.posting.posts.data.remote.FeedsApi import com.isolaatti.posting.posts.data.remote.FeedsApi
import com.isolaatti.posting.posts.data.remote.PostApi
import com.isolaatti.posting.posts.data.repository.PostsRepositoryImpl import com.isolaatti.posting.posts.data.repository.PostsRepositoryImpl
import com.isolaatti.posting.posts.domain.PostsRepository import com.isolaatti.posting.posts.domain.PostsRepository
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import retrofit2.create
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@ -18,7 +20,12 @@ class Module {
} }
@Provides @Provides
fun providePostsRepository(feedsApi: FeedsApi): PostsRepository { fun providePostApi(retrofitClient: RetrofitClient): PostApi {
return PostsRepositoryImpl(feedsApi) return retrofitClient.client.create(PostApi::class.java)
}
@Provides
fun providePostsRepository(feedsApi: FeedsApi, postApi: PostApi): PostsRepository {
return PostsRepositoryImpl(feedsApi, postApi)
} }
} }

View File

@ -0,0 +1,8 @@
package com.isolaatti.posting.posts.data.remote
data class CreatePostDto(
val privacy: Int,
val content: String,
val audioId: String?,
val squadId: String?
)

View File

@ -0,0 +1,3 @@
package com.isolaatti.posting.posts.data.remote
data class DeletePostDto(val id: Long)

View File

@ -0,0 +1,15 @@
package com.isolaatti.posting.posts.data.remote
data class EditPostDto(
val privacy: Int,
val content: String,
val audioId: String?,
val squadId: String?,
val postId: Long
) {
companion object {
const val PRIVACY_PRIVATE = 1
const val PRIVACY_ISOLAATTI = 2
const val PRIVACY_PUBLIC = 3
}
}

View File

@ -1,5 +1,9 @@
package com.isolaatti.posting.posts.data.remote package com.isolaatti.posting.posts.data.remote
import android.os.Parcel
import android.os.Parcelable
import java.io.Serializable
data class FeedDto( data class FeedDto(
val data: MutableList<PostDto>, val data: MutableList<PostDto>,
var moreContent: Boolean var moreContent: Boolean
@ -20,17 +24,89 @@ data class FeedDto(
val userName: String, val userName: String,
val squadName: String?, val squadName: String?,
var liked: Boolean var liked: Boolean
) { ): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readParcelable(Post::class.java.classLoader)!!,
parcel.readInt(),
parcel.readInt(),
parcel.readString()!!,
parcel.readString(),
parcel.readByte() != 0.toByte()
)
data class Post( data class Post(
val id: Long, val id: Long,
var textContent: String, var textContent: String,
val userId: Int, val userId: Int,
val privacy: Int, val privacy: Int,
val date: String, val date: String,
var audioId: String, var audioId: String?,
val squadId: String, val squadId: String?,
val linkedDiscussionId: Long, val linkedDiscussionId: Long,
val linkedCommentId: Long val linkedCommentId: Long
) ) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readLong(),
parcel.readString() ?: "",
parcel.readInt(),
parcel.readInt(),
parcel.readString() ?: "",
parcel.readString(),
parcel.readString(),
parcel.readLong(),
parcel.readLong()
) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeLong(id)
parcel.writeString(textContent)
parcel.writeInt(userId)
parcel.writeInt(privacy)
parcel.writeString(date)
parcel.writeString(audioId)
parcel.writeString(squadId)
parcel.writeLong(linkedDiscussionId)
parcel.writeLong(linkedCommentId)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<Post> {
override fun createFromParcel(parcel: Parcel): Post {
return Post(parcel)
}
override fun newArray(size: Int): Array<Post?> {
return arrayOfNulls(size)
}
}
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(post, flags)
parcel.writeInt(numberOfLikes)
parcel.writeInt(numberOfComments)
parcel.writeString(userName)
parcel.writeString(squadName)
parcel.writeByte(if (liked) 1 else 0)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<PostDto> {
override fun createFromParcel(parcel: Parcel): PostDto {
return PostDto(parcel)
}
override fun newArray(size: Int): Array<PostDto?> {
return arrayOfNulls(size)
}
}
} }
} }

View File

@ -0,0 +1,17 @@
package com.isolaatti.posting.posts.data.remote
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST
interface PostApi {
@POST("Posting/Make")
fun makePost(@Body post: CreatePostDto): Call<FeedDto.PostDto>
@POST("Posting/Edit")
fun editPost(@Body editedPost: EditPostDto): Call<FeedDto.PostDto>
@POST("Posting/Delete")
fun deletePost(@Body postToDelete: DeletePostDto): Call<Any>
}

View File

@ -1,9 +1,12 @@
package com.isolaatti.posting.posts.data.repository package com.isolaatti.posting.posts.data.repository
import com.google.gson.Gson import com.google.gson.Gson
import com.isolaatti.posting.posts.data.remote.CreatePostDto
import com.isolaatti.posting.posts.data.remote.EditPostDto
import com.isolaatti.posting.posts.data.remote.FeedDto import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.posting.posts.data.remote.FeedFilterDto import com.isolaatti.posting.posts.data.remote.FeedFilterDto
import com.isolaatti.posting.posts.data.remote.FeedsApi import com.isolaatti.posting.posts.data.remote.FeedsApi
import com.isolaatti.posting.posts.data.remote.PostApi
import com.isolaatti.posting.posts.domain.PostsRepository import com.isolaatti.posting.posts.domain.PostsRepository
import com.isolaatti.utils.Resource import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -16,7 +19,7 @@ import javax.inject.Inject
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi) : PostsRepository { class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, private val postApi: PostApi) : PostsRepository {
override fun getFeed(lastId: Long): Flow<Resource<FeedDto>> = flow { override fun getFeed(lastId: Long): Flow<Resource<FeedDto>> = flow {
emit(Resource.Loading()) emit(Resource.Loading())
try { try {
@ -55,4 +58,42 @@ class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi) :
emit(Resource.Error(Resource.Error.ErrorType.NetworkError)) emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
} }
} }
override fun makePost(createPostDto: CreatePostDto): Flow<Resource<FeedDto.PostDto>> = flow {
emit(Resource.Loading())
try {
val result = postApi.makePost(createPostDto).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 editPost(editPostDto: EditPostDto): Flow<Resource<FeedDto.PostDto>> = flow {
emit(Resource.Loading())
try {
val result = postApi.editPost(editPostDto).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))
}
}
} }

View File

@ -1,5 +1,7 @@
package com.isolaatti.posting.posts.domain package com.isolaatti.posting.posts.domain
import com.isolaatti.posting.posts.data.remote.CreatePostDto
import com.isolaatti.posting.posts.data.remote.EditPostDto
import com.isolaatti.posting.posts.data.remote.FeedDto import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.posting.posts.data.remote.FeedFilterDto import com.isolaatti.posting.posts.data.remote.FeedFilterDto
import com.isolaatti.utils.Resource import com.isolaatti.utils.Resource
@ -10,4 +12,7 @@ interface PostsRepository {
fun getFeed(lastId: Long): Flow<Resource<FeedDto>> fun getFeed(lastId: Long): Flow<Resource<FeedDto>>
fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto): Flow<Resource<FeedDto>> fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto): Flow<Resource<FeedDto>>
fun makePost(createPostDto: CreatePostDto): Flow<Resource<FeedDto.PostDto>>
fun editPost(editPostDto: EditPostDto): Flow<Resource<FeedDto.PostDto>>
} }

View File

@ -0,0 +1,20 @@
package com.isolaatti.posting.posts.domain.use_case
import com.isolaatti.posting.posts.data.remote.EditPostDto
import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.posting.posts.domain.PostsRepository
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class EditPost @Inject constructor(private val postsRepository: PostsRepository) {
operator fun invoke(
postId: Long,
privacy: Int,
content: String,
audioId: String?,
squadId: String?
): Flow<Resource<FeedDto.PostDto>> {
return postsRepository.editPost(EditPostDto(privacy, content, audioId, squadId, postId))
}
}

View File

@ -1,4 +1,19 @@
package com.isolaatti.posting.posts.domain.use_case package com.isolaatti.posting.posts.domain.use_case
class MakePost { import com.isolaatti.posting.posts.data.remote.CreatePostDto
import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.posting.posts.domain.PostsRepository
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class MakePost @Inject constructor(private val postsRepository: PostsRepository) {
operator fun invoke(
privacy: Int,
content: String,
audioId: String?,
squadId: String?
): Flow<Resource<FeedDto.PostDto>> {
return postsRepository.makePost(CreatePostDto(privacy, content, audioId, squadId))
}
} }

View File

@ -0,0 +1,67 @@
package com.isolaatti.posting.posts.presentation
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.isolaatti.posting.posts.data.remote.CreatePostDto
import com.isolaatti.posting.posts.data.remote.EditPostDto
import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.posting.posts.domain.use_case.EditPost
import com.isolaatti.posting.posts.domain.use_case.MakePost
import com.isolaatti.utils.Resource
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
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 CreatePostViewModel @Inject constructor(private val makePost: MakePost, private val editPost: EditPost) : ViewModel() {
val validation: MutableLiveData<Boolean> = MutableLiveData(false)
val posted: MutableLiveData<FeedDto.PostDto?> = MutableLiveData()
val error: MutableLiveData<Resource.Error.ErrorType?> = MutableLiveData()
val loading: MutableLiveData<Boolean> = MutableLiveData(false)
fun postDiscussion(content: String) {
viewModelScope.launch {
makePost(EditPostDto.PRIVACY_ISOLAATTI, content, null, null).onEach {
when(it) {
is Resource.Success -> {
loading.postValue(false)
posted.postValue(it.data)
}
is Resource.Error -> {
loading.postValue(false)
error.postValue(it.errorType)
}
is Resource.Loading -> {
loading.postValue(true)
}
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
fun editDiscussion(postId: Long, content: String) {
viewModelScope.launch {
editPost(postId, EditPostDto.PRIVACY_ISOLAATTI, content, null, null).onEach {
when(it) {
is Resource.Success -> {
loading.postValue(false)
posted.postValue(it.data)
}
is Resource.Error -> {
loading.postValue(false)
error.postValue(it.errorType)
}
is Resource.Loading -> {
loading.postValue(true)
}
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
}

View File

@ -0,0 +1,117 @@
package com.isolaatti.posting.posts.ui
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.os.PersistableBundle
import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat
import androidx.core.widget.doOnTextChanged
import com.isolaatti.R
import com.isolaatti.common.IsolaattiBaseActivity
import com.isolaatti.databinding.ActivityCreatePostBinding
import com.isolaatti.posting.posts.presentation.CreatePostViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class CreatePostActivity : IsolaattiBaseActivity() {
companion object {
const val EXTRA_MODE_CREATE = 0
/**
* Post activity in edit mode
*/
const val EXTRA_MODE_EDIT = 1
const val EXTRA_KEY_MODE = "mode"
/**
* post id, pass long
*/
const val EXTRA_KEY_POST_ID = "postId"
const val EXTRA_KEY_POST_POSTED = "post"
}
lateinit var binding: ActivityCreatePostBinding
val viewModel: CreatePostViewModel by viewModels()
var mode: Int = EXTRA_MODE_CREATE
var postId: Long = 0L
override fun onRetry() {
if(mode == EXTRA_MODE_EDIT && postId != 0L) {
viewModel.editDiscussion(postId, binding.filledTextField.editText?.text.toString())
} else {
viewModel.postDiscussion(binding.filledTextField.editText?.text.toString())
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent.extras?.let {
mode = it.getInt(EXTRA_KEY_MODE, EXTRA_MODE_CREATE)
postId = it.getLong(EXTRA_KEY_POST_ID)
}
binding = ActivityCreatePostBinding.inflate(layoutInflater)
setupUI()
setListeners()
setObservers()
setContentView(binding.root)
}
private fun setupUI() {
binding.toolbar.setTitle(if(mode == EXTRA_MODE_EDIT && postId != 0L) R.string.edit else R.string.new_post)
binding.filledTextField.requestFocus()
}
private fun setListeners() {
binding.toolbar.setNavigationOnClickListener {
exit()
}
binding.filledTextField.editText?.doOnTextChanged { text, _, _, _ ->
// make better validation :)
viewModel.validation.postValue(!text.isNullOrEmpty())
}
binding.postButton.setOnClickListener {
if(mode == EXTRA_MODE_EDIT && postId != 0L) {
viewModel.editDiscussion(postId, binding.filledTextField.editText?.text.toString())
} else {
viewModel.postDiscussion(binding.filledTextField.editText?.text.toString())
}
}
}
private fun setObservers() {
viewModel.validation.observe(this@CreatePostActivity) {
binding.postButton.isEnabled = it
}
viewModel.error.observe(this@CreatePostActivity) {
errorViewModel.error.postValue(it)
}
viewModel.posted.observe(this@CreatePostActivity) {
setResult(Activity.RESULT_OK, Intent().apply{
putExtra(EXTRA_KEY_POST_POSTED, it)
})
finish()
}
viewModel.loading.observe(this@CreatePostActivity) {
binding.progressBarLoading.visibility = if(it) View.VISIBLE else View.GONE
}
}
private fun exit() {
setResult(Activity.RESULT_CANCELED)
finish()
}
}

View File

@ -1,4 +0,0 @@
package com.isolaatti.posting.posts.ui
class CreatePostDialogFragment {
}

View File

@ -1,12 +1,12 @@
package com.isolaatti.profile.data.remote package com.isolaatti.profile.data.remote
import retrofit2.Call import retrofit2.Call
import retrofit2.http.GET import retrofit2.http.POST
import retrofit2.http.Path import retrofit2.http.Path
interface ProfileApi { interface ProfileApi {
@GET("Fetch/UserProfile/{userId}") @POST("Fetch/UserProfile/{userId}")
fun userProfile(@Path("userId") userId: Int): Call<UserProfileDto> fun userProfile(@Path("userId") userId: Int): Call<UserProfileDto>
} }

View File

@ -3,13 +3,29 @@ package com.isolaatti.profile.data.repository
import com.isolaatti.profile.data.remote.ProfileApi import com.isolaatti.profile.data.remote.ProfileApi
import com.isolaatti.profile.data.remote.UserProfileDto import com.isolaatti.profile.data.remote.UserProfileDto
import com.isolaatti.profile.domain.ProfileRepository import com.isolaatti.profile.domain.ProfileRepository
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import retrofit2.await import retrofit2.await
import javax.inject.Inject import javax.inject.Inject
class ProfileRepositoryImpl @Inject constructor(private val profileApi: ProfileApi) : ProfileRepository { class ProfileRepositoryImpl @Inject constructor(private val profileApi: ProfileApi) : ProfileRepository {
override fun getProfile(): Flow<UserProfileDto> = flow { override fun getProfile(userId: Int): Flow<Resource<UserProfileDto>> = flow {
try {
val result = profileApi.userProfile(userId).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))
}
} }
} }

View File

@ -2,8 +2,9 @@ package com.isolaatti.profile.domain
import com.isolaatti.profile.data.remote.ProfileApi import com.isolaatti.profile.data.remote.ProfileApi
import com.isolaatti.profile.data.remote.UserProfileDto import com.isolaatti.profile.data.remote.UserProfileDto
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface ProfileRepository { interface ProfileRepository {
fun getProfile(): Flow<UserProfileDto> fun getProfile(userId: Int): Flow<Resource<UserProfileDto>>
} }

View File

@ -1,4 +1,11 @@
package com.isolaatti.profile.domain.use_case package com.isolaatti.profile.domain.use_case
class GetProfile { import com.isolaatti.profile.data.remote.UserProfileDto
import com.isolaatti.profile.domain.ProfileRepository
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class GetProfile @Inject constructor(private val profileRepository: ProfileRepository) {
operator fun invoke(userId: Int): Flow<Resource<UserProfileDto>> = profileRepository.getProfile(userId)
} }

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@color/on_surface" android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
</vector>

View File

@ -1,5 +1,5 @@
<vector android:height="24dp" android:tint="#000000" <vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24" android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/> <path android:fillColor="@color/on_surface" android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
</vector> </vector>

View File

@ -1,9 +1,9 @@
<vector android:height="24dp" android:tint="#000000" <vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24" android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M17,19.22H5V7h7V5H5C3.9,5 3,5.9 3,7v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-7h-2V19.22z"/> <path android:fillColor="@color/on_surface" android:pathData="M17,19.22H5V7h7V5H5C3.9,5 3,5.9 3,7v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-7h-2V19.22z"/>
<path android:fillColor="@android:color/white" android:pathData="M19,2h-2v3h-3c0.01,0.01 0,2 0,2h3v2.99c0.01,0.01 2,0 2,0V7h3V5h-3V2z"/> <path android:fillColor="@color/on_surface" android:pathData="M19,2h-2v3h-3c0.01,0.01 0,2 0,2h3v2.99c0.01,0.01 2,0 2,0V7h3V5h-3V2z"/>
<path android:fillColor="@android:color/white" android:pathData="M7,9h8v2h-8z"/> <path android:fillColor="@color/on_surface" android:pathData="M7,9h8v2h-8z"/>
<path android:fillColor="@android:color/white" android:pathData="M7,12l0,2l8,0l0,-2l-3,0z"/> <path android:fillColor="@color/on_surface" android:pathData="M7,12l0,2l8,0l0,-2l-3,0z"/>
<path android:fillColor="@android:color/white" android:pathData="M7,15h8v2h-8z"/> <path android:fillColor="@color/on_surface" android:pathData="M7,15h8v2h-8z"/>
</vector> </vector>

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:navigationIcon="@drawable/baseline_close_24"
app:navigationIconTint="@color/on_surface"
app:title="@string/new_post"
app:titleCentered="true">
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="100dp"
android:layout_marginTop="?attr/actionBarSize"
android:clipToPadding="false">
<com.google.android.material.textfield.TextInputLayout
style="?attr/textInputFilledStyle"
android:id="@+id/filledTextField"
android:layout_width="match_parent"
app:boxBackgroundMode="none"
android:layout_height="wrap_content"
app:hintEnabled="false">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/what_do_you_want_to_talk_about_markdown_is_compatible_you_can_record_an_audio_if_you_want"/>
</com.google.android.material.textfield.TextInputLayout>
</androidx.core.widget.NestedScrollView>
<ProgressBar
android:id="@+id/progress_bar_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
tools:visibility="visible"/>
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/bottomAppBar"
style="@style/Widget.Material3.BottomAppBar"
app:menu="@menu/discussion_creator_menu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/postButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/baseline_send_24"
app:layout_anchor="@id/bottomAppBar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,15 +1,48 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:background="@color/purple">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/profileImageView"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginStart="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
tools:srcCompat="@tools:sample/avatars" />
<TextView <TextView
android:layout_width="wrap_content" android:id="@+id/textViewName"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="24dp" android:layout_marginStart="8dp"
android:layout_marginStart="24dp" android:layout_marginTop="16dp"
android:layout_marginEnd="24dp" android:text="TextView"
android:textAppearance="?attr/textAppearanceHeadlineSmall" android:textAlignment="textStart"
android:textColor="?attr/colorOnSurface" android:textColor="@android:color/white"
android:text="@string/app_name" /> app:layout_constraintEnd_toEndOf="parent"
</LinearLayout> app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/profileImageView"
app:layout_constraintTop_toTopOf="@+id/profileImageView"
tools:text="Name" />
<TextView
android:id="@+id/textViewEmail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="TextView"
android:textAlignment="textStart"
android:textColor="@android:color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/profileImageView"
app:layout_constraintTop_toBottomOf="@+id/textViewName" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:title="@string/add_audio"
android:icon="@drawable/baseline_mic_24"
app:showAsAction="ifRoom"/>
<item android:title="@string/add_image"
android:icon="@drawable/baseline_image_24"
app:showAsAction="ifRoom"/>
</menu>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:icon="@drawable/baseline_add_24"
app:showAsAction="always"
android:title="@string/new_post" />
</menu>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="purple">#4d3b68</color> <color name="purple">#4d3b68</color>
<color name="purple_lighter">#7015ea</color> <color name="purple_lighter">#3F0095</color>
<color name="surface">#1D1725</color> <color name="surface">#1D1725</color>
<color name="on_surface">#FFFFFF</color> <color name="on_surface">#FFFFFF</color>
</resources> </resources>

View File

@ -43,4 +43,8 @@
<string name="add_audio">New audio</string> <string name="add_audio">New audio</string>
<string name="load_more">Load more</string> <string name="load_more">Load more</string>
<string name="there_is_no_more_content_to_show">There is no more content to show</string> <string name="there_is_no_more_content_to_show">There is no more content to show</string>
<string name="post">Post</string>
<string name="add_image">Add image</string>
<string name="what_do_you_want_to_talk_about_markdown_is_compatible_you_can_record_an_audio_if_you_want">What do you want to talk about? Markdown is compatible. You can record an audio if you want.</string>
<string name="posted_successfully">Posted!</string>
</resources> </resources>