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=".profile.ui.ProfileActivity" 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>
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

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

View File

@ -1,13 +1,17 @@
package com.isolaatti.auth
import android.content.Context
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.remote.AuthApi
import com.isolaatti.auth.domain.AuthRepository
import com.isolaatti.connectivity.RetrofitClient
import com.isolaatti.database.AppDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
@Module
@ -18,7 +22,12 @@ class Module {
return retrofitClient.client.create(AuthApi::class.java)
}
@Provides
fun provideAuthRepository(tokenStorage: TokenStorage, authApi: AuthApi): AuthRepository {
return AuthRepositoryImpl(tokenStorage, authApi)
fun provideKeyValueDao(database: AppDatabase): KeyValueDao {
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
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.local.TokenStorage
import com.isolaatti.auth.data.remote.AuthApi
@ -8,16 +10,18 @@ import com.isolaatti.auth.domain.AuthRepository
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import retrofit2.await
import retrofit2.awaitResponse
import java.io.IOException
import javax.inject.Inject
class AuthRepositoryImpl @Inject constructor(
private val tokenStorage: TokenStorage,
private val authApi: AuthApi
private val authApi: AuthApi,
private val keyValueDao: KeyValueDao
) : AuthRepository {
companion object {
val KEY_USERID = "user_id"
}
override fun authWithEmailAndPassword(
email: String,
password: String
@ -33,6 +37,7 @@ class AuthRepositoryImpl @Inject constructor(
return@flow
}
tokenStorage.storeToken(dto)
keyValueDao.setValue(KeyValueEntity(KEY_USERID, dto.userId.toString()))
emit(Resource.Success(true))
return@flow
}
@ -60,4 +65,8 @@ class AuthRepositoryImpl @Inject constructor(
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
data class AuthTokenDto(val token: String) {
data class AuthTokenDto(val token: String, val userId: Int) {
override fun toString(): String {
return token
}

View File

@ -8,4 +8,6 @@ interface AuthRepository {
fun authWithEmailAndPassword(email: String, password: String): Flow<Resource<Boolean>>
fun logout(): Flow<Boolean>
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.isolaatti.R
import com.isolaatti.home.HomeActivity
import com.isolaatti.login.LogInActivity
import com.isolaatti.utils.Resource
import dagger.hilt.android.AndroidEntryPoint
@ -46,7 +47,7 @@ abstract class IsolaattiBaseActivity : AppCompatActivity() {
abstract fun onRetry()
private val onAcceptReAuthClick = DialogInterface.OnClickListener { _, _ ->
signInActivityResult.launch(Intent(this, LogInActivity::class.java))
}
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
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
@ -7,20 +8,30 @@ import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
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.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.isolaatti.BuildConfig
import com.isolaatti.R
import com.isolaatti.common.ErrorMessageViewModel
import com.isolaatti.databinding.FragmentFeedBinding
import com.isolaatti.home.presentation.FeedViewModel
import com.isolaatti.posting.posts.presentation.PostsViewModel
import com.isolaatti.posting.comments.presentation.BottomSheetPostComments
import com.isolaatti.posting.common.domain.OnUserInteractedWithPostCallback
import com.isolaatti.posting.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment
import com.isolaatti.posting.posts.presentation.PostsRecyclerViewAdapter
import com.isolaatti.posting.posts.ui.CreatePostActivity
import com.isolaatti.profile.ui.ProfileActivity
import com.isolaatti.settings.ui.SettingsActivity
import com.isolaatti.utils.PicassoImagesPluginDef
import com.isolaatti.utils.UrlGen
import com.squareup.picasso.Picasso
import dagger.hilt.android.AndroidEntryPoint
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
@ -37,9 +48,16 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
private val viewModel: PostsViewModel by activityViewModels()
private val errorViewModel: ErrorMessageViewModel by activityViewModels()
private val screenViewModel: FeedViewModel by viewModels()
private lateinit var viewBinding: FragmentFeedBinding
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(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@ -99,6 +117,33 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
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){
if (it != null) {
@ -128,6 +173,7 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
PostsRecyclerViewAdapter.UpdateEvent.UpdateType.POST_LIKED, it.postId))
}
}
screenViewModel.getProfile()
}
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.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.domain.PostsRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.create
@Module
@InstallIn(SingletonComponent::class)
@ -18,7 +20,12 @@ class Module {
}
@Provides
fun providePostsRepository(feedsApi: FeedsApi): PostsRepository {
return PostsRepositoryImpl(feedsApi)
fun providePostApi(retrofitClient: RetrofitClient): PostApi {
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
import android.os.Parcel
import android.os.Parcelable
import java.io.Serializable
data class FeedDto(
val data: MutableList<PostDto>,
var moreContent: Boolean
@ -20,17 +24,89 @@ data class FeedDto(
val userName: String,
val squadName: String?,
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(
val id: Long,
var textContent: String,
val userId: Int,
val privacy: Int,
val date: String,
var audioId: String,
val squadId: String,
var audioId: String?,
val squadId: String?,
val linkedDiscussionId: 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
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.FeedFilterDto
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.utils.Resource
import kotlinx.coroutines.flow.Flow
@ -16,7 +19,7 @@ import javax.inject.Inject
import kotlin.coroutines.resume
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 {
emit(Resource.Loading())
try {
@ -55,4 +58,42 @@ class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi) :
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
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.FeedFilterDto
import com.isolaatti.utils.Resource
@ -10,4 +12,7 @@ interface PostsRepository {
fun getFeed(lastId: Long): 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
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
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
interface ProfileApi {
@GET("Fetch/UserProfile/{userId}")
@POST("Fetch/UserProfile/{userId}")
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.UserProfileDto
import com.isolaatti.profile.domain.ProfileRepository
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import retrofit2.await
import javax.inject.Inject
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.UserProfileDto
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
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
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: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>

View File

@ -1,9 +1,9 @@
<vector android:height="24dp" android:tint="#000000"
<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="@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="@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="@android:color/white" android:pathData="M7,9h8v2h-8z"/>
<path android:fillColor="@android:color/white" 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="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="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="M7,9h8v2h-8z"/>
<path android:fillColor="@color/on_surface" android:pathData="M7,12l0,2l8,0l0,-2l-3,0z"/>
<path android:fillColor="@color/on_surface" android:pathData="M7,15h8v2h-8z"/>
</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"?>
<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_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
android:layout_width="wrap_content"
android:id="@+id/textViewName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:textAppearance="?attr/textAppearanceHeadlineSmall"
android:textColor="?attr/colorOnSurface"
android:text="@string/app_name" />
</LinearLayout>
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:text="TextView"
android:textAlignment="textStart"
android:textColor="@android:color/white"
app:layout_constraintEnd_toEndOf="parent"
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"?>
<resources>
<color name="purple">#4d3b68</color>
<color name="purple_lighter">#7015ea</color>
<color name="purple_lighter">#3F0095</color>
<color name="surface">#1D1725</color>
<color name="on_surface">#FFFFFF</color>
</resources>

View File

@ -43,4 +43,8 @@
<string name="add_audio">New audio</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="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>