This commit is contained in:
Erik Cavazos 2023-07-08 02:17:19 -06:00
parent a51105c334
commit 86dc367837
86 changed files with 1426 additions and 321 deletions

46
.idea/assetWizardSettings.xml generated Normal file
View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="WizardSettings">
<option name="children">
<map>
<entry key="vectorWizard">
<value>
<PersistentState>
<option name="children">
<map>
<entry key="vectorAssetStep">
<value>
<PersistentState>
<option name="children">
<map>
<entry key="clipartAsset">
<value>
<PersistentState>
<option name="values">
<map>
<entry key="url" value="file:/$USER_HOME$/AppData/Local/Android/Sdk/icons/material/materialicons/report/baseline_report_24.xml" />
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
<option name="values">
<map>
<entry key="outputName" value="baseline_report_24" />
<entry key="sourceFile" value="C:\Users\erike\Downloads\comments-solid.svg" />
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
</component>
</project>

2
.idea/compiler.xml generated
View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
<bytecodeTargetLevel target="17" />
</component>
</project>

3
.idea/misc.xml generated
View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

6
.idea/render.experimental.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RenderSettings">
<option name="showDecorations" value="true" />
</component>
</project>

View File

@ -40,8 +40,9 @@ android {
dependencies {
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.0'
implementation "androidx.recyclerview:recyclerview:1.3.0"
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
@ -66,7 +67,7 @@ dependencies {
implementation "com.google.android.material:material:1.8.0"
// Navigation
def nav_version = "2.5.3"
def nav_version = "2.6.0"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"
@ -81,6 +82,9 @@ dependencies {
// Markwon
final def markwon_version = '4.6.2'
// Customtabs
implementation 'androidx.browser:browser:1.5.0'
implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:editor:$markwon_version"
implementation "io.noties.markwon:image-picasso:$markwon_version"

View File

@ -1,8 +0,0 @@
package com.isolaatti.comments.data.remote
import com.isolaatti.comments.domain.model.Comment
data class CommentDto(
val comment: Comment,
val username: String
)

View File

@ -1,10 +0,0 @@
package com.isolaatti.comments.data.remote
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST
interface CommentsApi {
@POST("Posting/Post/{postId}/Comment")
fun postComment(@Body commentToPost: CommentToPostDto): Call<Nothing>
}

View File

@ -1,8 +0,0 @@
package com.isolaatti.comments.data.repository
import com.isolaatti.comments.data.remote.CommentsApi
import com.isolaatti.comments.domain.CommentsRepository
import javax.inject.Inject
class CommentsRepositoryImpl @Inject constructor(private val commentsApi: CommentsApi) : CommentsRepository {
}

View File

@ -1,5 +0,0 @@
package com.isolaatti.comments.domain
interface CommentsRepository {
}

View File

@ -3,7 +3,6 @@ package com.isolaatti.connectivity
import com.isolaatti.auth.domain.AuthRepository
import okhttp3.Interceptor
import okhttp3.Response
class AuthenticationInterceptor(private val authRepository: dagger.Lazy<AuthRepository>) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
@ -16,7 +15,10 @@ class AuthenticationInterceptor(private val authRepository: dagger.Lazy<AuthRepo
// Add auth header here
val tokenDto = authRepository.get().getCurrentToken()
tokenDto?.token?.let {
val request = chain.request().newBuilder().addHeader("sessionToken", it) .build()
val request = chain.request().newBuilder()
.addHeader("Authorization", it)
.addHeader("client-id", ClientId.guid.toString())
.build()
return chain.proceed(request)
}

View File

@ -0,0 +1,7 @@
package com.isolaatti.connectivity
import java.util.UUID
object ClientId {
val guid: UUID get() = UUID.randomUUID()
}

View File

@ -0,0 +1,5 @@
package com.isolaatti.connectivity
object NetworkStatus {
var networkIsAvailable: Boolean = true
}

View File

@ -3,6 +3,7 @@ package com.isolaatti.connectivity
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.IOException
import javax.inject.Inject

View File

@ -1,4 +1,4 @@
package com.isolaatti.home.feed.data.remote
package com.isolaatti.feed.data.remote
import retrofit2.Call
import retrofit2.http.GET

View File

@ -0,0 +1,6 @@
package com.isolaatti.feed.data.remote
data class FeedDto(
val data: MutableList<PostDto>,
val moreContent: Boolean
)

View File

@ -0,0 +1,12 @@
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
)

View File

@ -1,8 +1,8 @@
package com.isolaatti.home.feed.data.repository
package com.isolaatti.feed.data.repository
import com.isolaatti.home.feed.data.remote.FeedApi
import com.isolaatti.home.feed.data.remote.FeedDto
import com.isolaatti.home.feed.domain.repository.FeedRepository
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

View File

@ -1,12 +1,12 @@
package com.isolaatti.home.feed.domain.model
package com.isolaatti.feed.domain.model
data class Post(
val id: Long,
val textContent: String,
var textContent: String,
val userId: Int,
val privacy: Int,
val date: String,
val audioId: String,
var audioId: String,
val squadId: String,
val linkedDiscussionId: Long,
val linkedCommentId: Long

View File

@ -1,6 +1,6 @@
package com.isolaatti.home.feed.domain.repository
package com.isolaatti.feed.domain.repository
import com.isolaatti.home.feed.data.remote.FeedDto
import com.isolaatti.feed.data.remote.FeedDto
import kotlinx.coroutines.flow.Flow
interface FeedRepository {

View File

@ -1,8 +1,8 @@
package com.isolaatti.home.feed.ui
package com.isolaatti.feed.ui
import android.content.Intent
import androidx.lifecycle.ViewModelProvider
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
@ -11,7 +11,11 @@ import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.isolaatti.R
import com.isolaatti.databinding.FragmentFeedBinding
import com.isolaatti.home.feed.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.profile.ui.ProfileActivity
import com.isolaatti.settings.ui.SettingsActivity
import com.isolaatti.utils.PicassoImagesPluginDef
@ -23,15 +27,15 @@ import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAb
import io.noties.markwon.linkify.LinkifyPlugin
@AndroidEntryPoint
class FeedFragment : Fragment() {
class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
companion object {
fun newInstance() = FeedFragment()
}
private val viewModel: FeedViewModel by activityViewModels()
private val viewModel: PostsViewModel by activityViewModels()
private lateinit var viewBinding: FragmentFeedBinding
private lateinit var adapter: FeedRecyclerViewAdapter
private lateinit var adapter: PostsRecyclerViewAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
@ -45,9 +49,10 @@ class FeedFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewBinding.topAppBar.setNavigationOnClickListener {
viewBinding.drawerLayout.openDrawer(viewBinding.homeDrawer)
viewBinding.drawerLayout?.openDrawer(viewBinding.homeDrawer)
}
viewBinding.homeDrawer.setNavigationItemSelectedListener {
when(it.itemId) {
R.id.my_profile_menu_item -> {
@ -73,12 +78,37 @@ class FeedFragment : Fragment() {
.usePlugin(PicassoImagesPluginDef.picassoImagePlugin)
.usePlugin(LinkifyPlugin.create())
.build()
adapter = FeedRecyclerViewAdapter(markwon)
adapter = PostsRecyclerViewAdapter(markwon, this, listOf())
viewBinding.feedRecyclerView.adapter = adapter
viewBinding.feedRecyclerView.layoutManager = LinearLayoutManager(requireContext())
viewModel.feed.observe(viewLifecycleOwner){
adapter.submitList(it.data)
viewModel.posts.observe(viewLifecycleOwner){
Log.d("recycler", it.data.toString())
adapter.updateList(it.data.toList(),null)
}
viewModel.postLiked.observe(viewLifecycleOwner) {
adapter.updateList(viewModel.posts.value?.data, PostsRecyclerViewAdapter.UpdateEvent(
PostsRecyclerViewAdapter.UpdateEvent.UpdateType.POST_LIKED, it.postId))
}
}
override fun onLiked(postId: Long) = viewModel.likePost(postId)
override fun onUnLiked(postId: Long) = viewModel.unLikePost(postId)
override fun onOptions(postId: Long) {
val modalBottomSheet = BottomSheetPostOptionsFragment()
modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostOptionsFragment.TAG)
}
override fun onComment(postId: Long) {
val modalBottomSheet = BottomSheetPostComments.getInstance(postId)
modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostComments.TAG)
}
override fun onProfileClick(userId: Int) {
TODO("Not yet implemented")
}
}

View File

@ -1,8 +0,0 @@
package com.isolaatti.followers.data
import com.isolaatti.followers.data.remote.FollowersApi
import com.isolaatti.followers.domain.FollowersRepository
import javax.inject.Inject
class FollowersRepositoryImpl @Inject constructor(followersApi: FollowersApi) : FollowersRepository {
}

View File

@ -0,0 +1,33 @@
package com.isolaatti.followers.data
import com.isolaatti.followers.data.remote.FollowersApi
import com.isolaatti.followers.domain.FollowersRepository
import com.isolaatti.profile.data.remote.ProfileListItemDto
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import retrofit2.awaitResponse
import javax.inject.Inject
class FollowersRepositoryImpl @Inject constructor(private val followersApi: FollowersApi) : FollowersRepository {
override fun getFollowersOfUser(userId: Int): Flow<List<ProfileListItemDto>> = flow {
val response = followersApi.getFollowersOfUser(userId).awaitResponse()
if(response.isSuccessful) {
response.body()?.let { emit(response.body()!!) }
}
}
override fun getFollowingsOfUser(userId: Int): Flow<List<ProfileListItemDto>> = flow {
val response = followersApi.getFollowingsOfUser(userId).awaitResponse()
if(response.isSuccessful) {
}
}
override fun followUser(userId: Int): Flow<Boolean> = flow {
}
override fun unfollowUser(userId: Int): Flow<Boolean> = flow {
}
}

View File

@ -1,4 +1,11 @@
package com.isolaatti.followers.domain
import com.isolaatti.profile.data.remote.ProfileListItemDto
import kotlinx.coroutines.flow.Flow
interface FollowersRepository {
fun getFollowersOfUser(userId: Int): Flow<List<ProfileListItemDto>>
fun getFollowingsOfUser(userId: Int): Flow<List<ProfileListItemDto>>
fun followUser(userId: Int): Flow<Boolean>
fun unfollowUser(userId: Int): Flow<Boolean>
}

View File

@ -4,22 +4,17 @@ import android.os.Bundle
import android.view.Menu
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.app.ActivityCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.search.SearchBar
import com.isolaatti.R
import com.isolaatti.databinding.ActivityHomeBinding
import com.isolaatti.home.feed.presentation.FeedViewModel
import com.isolaatti.posting.posts.presentation.PostsViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class HomeActivity : AppCompatActivity() {
lateinit var viewBinding: ActivityHomeBinding
val feedViewModel: FeedViewModel by viewModels()
val postsViewModel: PostsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -28,7 +23,10 @@ class HomeActivity : AppCompatActivity() {
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
viewBinding.bottomNavigation.setupWithNavController(navHostFragment.navController)
feedViewModel.getFeed()
if(savedInstanceState == null) {
postsViewModel.getFeed()
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {

View File

@ -1,9 +1,9 @@
package com.isolaatti.home
import com.isolaatti.connectivity.RetrofitClient
import com.isolaatti.home.feed.data.remote.FeedApi
import com.isolaatti.home.feed.data.repository.FeedRepositoryImpl
import com.isolaatti.home.feed.domain.repository.FeedRepository
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

View File

@ -1,6 +0,0 @@
package com.isolaatti.home.feed.data.remote
data class FeedDto(
val data: List<PostDto>,
val moreContent: Boolean
)

View File

@ -1,12 +0,0 @@
package com.isolaatti.home.feed.data.remote
import com.isolaatti.home.feed.domain.model.Post
data class PostDto(
val post: Post,
val numberOfLikes: Int,
val numberOfComments: Int,
val userName: String,
val squadName: String?,
val liked: Boolean
)

View File

@ -1,29 +0,0 @@
package com.isolaatti.home.feed.presentation
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.isolaatti.home.feed.data.remote.FeedDto
import com.isolaatti.home.feed.domain.repository.FeedRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class FeedViewModel @Inject constructor(private val feedRepository: FeedRepository) : ViewModel() {
private val _feed: MutableLiveData<FeedDto> = MutableLiveData()
val feed: LiveData<FeedDto> get() = _feed
private fun getLastId(): Long = try {_feed.value?.data?.last()?.post?.id ?: 0} catch (e: NoSuchElementException) { 0 }
fun getFeed() {
viewModelScope.launch {
feedRepository.getNextPage(0, 20).collect {
_feed.postValue(it)
}
}
}
}

View File

@ -1,62 +0,0 @@
package com.isolaatti.home.feed.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.isolaatti.R
import com.isolaatti.home.feed.data.remote.PostDto
import com.isolaatti.utils.UrlGen.userProfileImage
import com.squareup.picasso.Picasso
import io.noties.markwon.Markwon
import java.text.DateFormat
import java.util.Date
class FeedRecyclerViewAdapter(private val markwon: Markwon) : ListAdapter<PostDto, FeedRecyclerViewAdapter.FeedViewHolder>(DIFF_CALLBACK) {
inner class FeedViewHolder(itemView: View) : ViewHolder(itemView) {
fun bindView(postDto: PostDto) {
val username: TextView = itemView.findViewById(R.id.text_view_username)
username.text = postDto.userName
val profileImageView: ImageView = itemView.findViewById(R.id.avatar_picture)
Picasso.get().load(userProfileImage(postDto.post.userId)).into(profileImageView)
val dateTextView: TextView = itemView.findViewById(R.id.text_view_date)
dateTextView.text = postDto.post.date
val content: TextView = itemView.findViewById(R.id.post_content)
markwon.setMarkdown(content, postDto.post.textContent)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FeedViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.post_layout, parent, false)
return FeedViewHolder(view)
}
override fun onBindViewHolder(holder: FeedViewHolder, position: Int) {
holder.bindView(getItem(position))
}
companion object {
val DIFF_CALLBACK = object: DiffUtil.ItemCallback<PostDto>() {
override fun areItemsTheSame(oldItem: PostDto, newItem: PostDto): Boolean {
return oldItem.post.id == newItem.post.id
}
override fun areContentsTheSame(oldItem: PostDto, newItem: PostDto): Boolean {
return oldItem.post.id == newItem.post.id && oldItem.post.textContent == newItem.post.textContent
}
}
}
}

View File

@ -1,9 +1,9 @@
package com.isolaatti.posts
package com.isolaatti.posting
import com.isolaatti.connectivity.RetrofitClient
import com.isolaatti.posts.data.remote.PostsApi
import com.isolaatti.posts.data.repository.PostsRepositoryImpl
import com.isolaatti.posts.domain.PostsRepository
import com.isolaatti.posting.posts.data.remote.PostsApi
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

View File

@ -1,8 +1,8 @@
package com.isolaatti.comments
package com.isolaatti.posting.comments
import com.isolaatti.comments.data.remote.CommentsApi
import com.isolaatti.comments.data.repository.CommentsRepositoryImpl
import com.isolaatti.comments.domain.CommentsRepository
import com.isolaatti.posting.comments.data.remote.CommentsApi
import com.isolaatti.posting.comments.data.repository.CommentsRepositoryImpl
import com.isolaatti.posting.comments.domain.CommentsRepository
import com.isolaatti.connectivity.RetrofitClient
import dagger.Module
import dagger.Provides

View File

@ -0,0 +1,8 @@
package com.isolaatti.posting.comments.data.remote
import com.isolaatti.posting.comments.domain.model.Comment
data class CommentDto(
val comment: Comment,
val username: String
)

View File

@ -1,4 +1,4 @@
package com.isolaatti.comments.data.remote
package com.isolaatti.posting.comments.data.remote
data class CommentToPostDto(
val content: String,

View File

@ -0,0 +1,19 @@
package com.isolaatti.posting.comments.data.remote
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
interface CommentsApi {
@POST("Posting/Post/{postId}/Comment")
fun postComment(@Body commentToPost: CommentToPostDto): Call<Nothing>
@GET("Fetch/Post/{postId}/Comments")
fun getCommentsOfPosts(@Path("postId") postId: Long, @Query("lastId") lastId: Long, @Query("take") count: Int): Call<FeedCommentsDto>
@GET("Fetch/Comments/{commentId}")
fun getComment(@Path("commentId") commentId: Long): Call<CommentDto>
}

View File

@ -0,0 +1,3 @@
package com.isolaatti.posting.comments.data.remote
data class FeedCommentsDto(val data: List<CommentDto>, val moreContent: Boolean)

View File

@ -0,0 +1,33 @@
package com.isolaatti.posting.comments.data.repository
import com.isolaatti.posting.comments.data.remote.CommentDto
import com.isolaatti.posting.comments.data.remote.CommentToPostDto
import com.isolaatti.posting.comments.data.remote.CommentsApi
import com.isolaatti.posting.comments.data.remote.FeedCommentsDto
import com.isolaatti.posting.comments.domain.CommentsRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import retrofit2.awaitResponse
import javax.inject.Inject
class CommentsRepositoryImpl @Inject constructor(private val commentsApi: CommentsApi) :
CommentsRepository {
override fun getComments(postId: Long, lastId: Long): Flow<FeedCommentsDto> = flow {
val response = commentsApi.getCommentsOfPosts(postId, lastId, 15).awaitResponse()
if(response.isSuccessful){
response.body()?.let { emit(it) }
}
}
override fun getComment(commentId: Long): Flow<CommentDto> = flow {
val response = commentsApi.getComment(commentId).awaitResponse()
if(response.isSuccessful) {
response.body()?.let { emit(it) }
}
}
override fun postComment(commentToPostDto: CommentToPostDto, postId: Long): Flow<Boolean> = flow {
val response = commentsApi.postComment(commentToPostDto).awaitResponse()
emit(response.isSuccessful)
}
}

View File

@ -0,0 +1,12 @@
package com.isolaatti.posting.comments.domain
import com.isolaatti.posting.comments.data.remote.CommentDto
import com.isolaatti.posting.comments.data.remote.CommentToPostDto
import com.isolaatti.posting.comments.data.remote.FeedCommentsDto
import kotlinx.coroutines.flow.Flow
interface CommentsRepository {
fun getComments(postId: Long, lastId: Long): Flow<FeedCommentsDto>
fun getComment(commentId: Long): Flow<CommentDto>
fun postComment(commentToPostDto: CommentToPostDto, postId: Long): Flow<Boolean>
}

View File

@ -1,4 +1,4 @@
package com.isolaatti.comments.domain.model
package com.isolaatti.posting.comments.domain.model
data class Comment(
val id: Long,

View File

@ -0,0 +1,12 @@
package com.isolaatti.posting.comments.domain.use_case
import com.isolaatti.posting.comments.data.remote.CommentDto
import com.isolaatti.posting.comments.data.remote.FeedCommentsDto
import com.isolaatti.posting.comments.domain.CommentsRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class GetComments @Inject constructor(private val commentsRepository: CommentsRepository) {
operator fun invoke(postId: Long, lastId: Long? = null): Flow<FeedCommentsDto> =
commentsRepository.getComments(postId, lastId ?: 0)
}

View File

@ -0,0 +1,100 @@
package com.isolaatti.posting.comments.presentation
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.isolaatti.databinding.BottomSheetPostCommentsBinding
import com.isolaatti.posting.common.domain.OnUserInteractedCallback
import com.isolaatti.posting.common.options_bottom_sheet.domain.Options
import com.isolaatti.posting.common.options_bottom_sheet.presentation.BottomSheetPostOptionsViewModel
import com.isolaatti.posting.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment
import com.isolaatti.utils.PicassoImagesPluginDef
import dagger.hilt.android.AndroidEntryPoint
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute
import io.noties.markwon.linkify.LinkifyPlugin
@AndroidEntryPoint
class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedCallback {
private lateinit var viewBinding: BottomSheetPostCommentsBinding
val viewModel: CommentsViewModel by viewModels()
val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val postId = arguments?.getLong(ARG_POST_ID)
if (postId != null) {
viewModel.postId = postId
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
viewBinding = BottomSheetPostCommentsBinding.inflate(inflater)
return viewBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewBinding.recyclerComments.isNestedScrollingEnabled = true
val markwon = Markwon.builder(requireContext())
.usePlugin(object: AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder
.imageDestinationProcessor(
ImageDestinationProcessorRelativeToAbsolute
.create("https://isolaatti.com/"))
}
})
.usePlugin(PicassoImagesPluginDef.picassoImagePlugin)
.usePlugin(LinkifyPlugin.create())
.build()
val adapter = CommentsRecyclerViewAdapter(listOf(), markwon, this)
viewBinding.recyclerComments.adapter = adapter
viewBinding.recyclerComments.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
viewModel.comments.observe(viewLifecycleOwner) {
adapter.submitList(it)
}
}
companion object {
const val TAG = "BottomSheetPostComments"
const val ARG_POST_ID = "postId"
fun getInstance(postId: Long): BottomSheetPostComments {
return BottomSheetPostComments().apply {
arguments = Bundle().apply {
putLong(ARG_POST_ID, postId)
}
}
}
}
override fun onOptions(postId: Long) {
val fragment = BottomSheetPostOptionsFragment()
fragment.show(parentFragmentManager, BottomSheetPostOptionsFragment.TAG)
optionsViewModel.setOptions(Options.commentOptions)
}
override fun onProfileClick(userId: Int) {
}
}

View File

@ -0,0 +1,45 @@
package com.isolaatti.posting.comments.presentation
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.isolaatti.databinding.CommentLayoutBinding
import com.isolaatti.posting.comments.data.remote.CommentDto
import com.isolaatti.posting.common.domain.OnUserInteractedCallback
import com.isolaatti.utils.UrlGen
import com.squareup.picasso.Picasso
import io.noties.markwon.Markwon
class CommentsRecyclerViewAdapter(private var list: List<CommentDto>, private val markwon: Markwon, private val callback: OnUserInteractedCallback) : RecyclerView.Adapter<CommentsRecyclerViewAdapter.CommentViewHolder>() {
inner class CommentViewHolder(val viewBinding: CommentLayoutBinding) : RecyclerView.ViewHolder(viewBinding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentViewHolder {
return CommentViewHolder(CommentLayoutBinding.inflate(LayoutInflater.from(parent.context)))
}
override fun getItemCount(): Int = list.count()
override fun onBindViewHolder(holder: CommentViewHolder, position: Int) {
val comment = list[position]
holder.viewBinding.textViewDate.text = comment.comment.date
markwon.setMarkdown(holder.viewBinding.postContent, comment.comment.textContent)
holder.viewBinding.textViewUsername.text = comment.username
holder.viewBinding.textViewUsername.setOnClickListener {
callback.onProfileClick(comment.comment.userId)
}
holder.viewBinding.moreButton.setOnClickListener {
callback.onOptions(comment.comment.id)
}
Picasso.get()
.load(UrlGen.userProfileImage(comment.comment.userId))
.into(holder.viewBinding.avatarPicture)
}
fun submitList(commentDtoList: List<CommentDto>) {
val lastIndex = if(list.count() - 1 < 1) 0 else list.count() - 1
list = commentDtoList
notifyItemRangeChanged(lastIndex, commentDtoList.count())
}
}

View File

@ -0,0 +1,58 @@
package com.isolaatti.posting.comments.presentation
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.isolaatti.posting.comments.data.remote.CommentDto
import com.isolaatti.posting.comments.domain.use_case.GetComments
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 CommentsViewModel @Inject constructor(private val getComments: GetComments) : ViewModel() {
private val _comments: MutableLiveData<List<CommentDto>> = MutableLiveData()
val comments: LiveData<List<CommentDto>> get() = _comments
/**
* postId to query comments for. First page will be fetched when set.
* Call getContent() to get more content if available
*/
var postId: Long = 0
set(value) {
field = value
getContent()
}
private var lastId: Long = 0L
fun getContent() {
viewModelScope.launch {
getComments(postId, lastId).onEach {
val newList = _comments.value?.toMutableList() ?: mutableListOf()
newList.addAll(it.data)
_comments.postValue(newList)
if(it.data.isNotEmpty()){
lastId = it.data.last().comment.id
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
/**
* Use when new comment has been posted
*/
fun putCommentAtTheBeginning(commentDto: CommentDto) {
val newList: MutableList<CommentDto> = mutableListOf(commentDto)
newList.addAll(_comments.value ?: mutableListOf())
_comments.postValue(newList)
}
}

View File

@ -0,0 +1,6 @@
package com.isolaatti.posting.common.domain
interface OnUserInteractedCallback {
fun onOptions(postId: Long)
fun onProfileClick(userId: Int)
}

View File

@ -0,0 +1,7 @@
package com.isolaatti.posting.common.domain
interface OnUserInteractedWithPostCallback : OnUserInteractedCallback {
fun onLiked(postId: Long)
fun onUnLiked(postId: Long)
fun onComment(postId: Long)
}

View File

@ -0,0 +1,29 @@
package com.isolaatti.posting.common.options_bottom_sheet.domain
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.isolaatti.R
data class Options(
@StringRes val title: Int,
val items: List<Option>
) {
data class Option(
@StringRes val stringRes: Int,
@DrawableRes val icon: Int
)
companion object {
val postOptions = Options(R.string.post_options_title, listOf(
Option(R.string.delete, R.drawable.baseline_delete_24),
Option(R.string.edit, R.drawable.baseline_edit_24),
Option(R.string.report, R.drawable.baseline_report_24)
))
val commentOptions = Options(R.string.post_options_title, listOf(
Option(R.string.delete, R.drawable.baseline_delete_24),
Option(R.string.edit, R.drawable.baseline_edit_24),
Option(R.string.report, R.drawable.baseline_report_24)
))
}
}

View File

@ -0,0 +1,15 @@
package com.isolaatti.posting.common.options_bottom_sheet.presentation
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.isolaatti.posting.common.options_bottom_sheet.domain.Options
class BottomSheetPostOptionsViewModel : ViewModel() {
private val _options: MutableLiveData<Options> = MutableLiveData()
val options: LiveData<Options> get() = _options
fun setOptions(options: Options) {
_options.postValue(options)
}
}

View File

@ -0,0 +1,59 @@
package com.isolaatti.posting.common.options_bottom_sheet.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ListAdapter
import android.widget.ListView
import androidx.appcompat.content.res.AppCompatResources
import androidx.fragment.app.activityViewModels
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton
import com.isolaatti.R
import com.isolaatti.databinding.BottomSheetPostOptionsBinding
import com.isolaatti.posting.common.options_bottom_sheet.domain.Options
import com.isolaatti.posting.common.options_bottom_sheet.presentation.BottomSheetPostOptionsViewModel
class BottomSheetPostOptionsFragment : BottomSheetDialogFragment() {
private lateinit var viewBinding: BottomSheetPostOptionsBinding
private val viewModel: BottomSheetPostOptionsViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
viewBinding = BottomSheetPostOptionsBinding.inflate(inflater)
return viewBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.options.observe(viewLifecycleOwner) {
renderOptions(it)
}
}
private fun renderOptions(options: Options) {
viewBinding.optionsContainer.removeAllViews()
for(option in options.items) {
val button = MaterialButton(requireContext(), null, com.google.android.material.R.style.Widget_Material3_Button_TextButton)
button.icon = AppCompatResources.getDrawable(requireContext(), option.icon)
button.text = requireContext().getText(option.stringRes)
button.textAlignment = MaterialButton.TEXT_ALIGNMENT_TEXT_START
viewBinding.optionsContainer.addView(button)
}
}
companion object {
const val TAG = "BottomSheetPostOptions"
}
}

View File

@ -0,0 +1,25 @@
package com.isolaatti.posting.likes
import com.isolaatti.connectivity.RetrofitClient
import com.isolaatti.posting.likes.data.LikesRepositoryImpl
import com.isolaatti.posting.likes.data.remote.LikesApi
import com.isolaatti.posting.likes.domain.repository.LikesRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
class Module {
@Provides
fun provideLikesApi(retrofitClient: RetrofitClient): LikesApi {
return retrofitClient.client.create(LikesApi::class.java)
}
@Provides
fun provideLikesRepository(likesApi: LikesApi): LikesRepository {
return LikesRepositoryImpl(likesApi)
}
}

View File

@ -0,0 +1,28 @@
package com.isolaatti.posting.likes.data
import android.util.Log
import com.isolaatti.posting.likes.data.remote.LikeDto
import com.isolaatti.posting.likes.data.remote.LikesApi
import com.isolaatti.posting.likes.domain.repository.LikesRepository
import com.isolaatti.utils.LongIdentificationWrapper
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import retrofit2.awaitResponse
class LikesRepositoryImpl(private val likesApi: LikesApi) : LikesRepository {
override fun likePost(postId: Long): Flow<LikeDto> = flow {
val response = likesApi.likePost(LongIdentificationWrapper(postId)).awaitResponse()
Log.d("likes_repo", response.toString())
if(response.isSuccessful) {
response.body()?.let { emit(it) }
}
}
override fun unLikePost(postId: Long): Flow<LikeDto> = flow {
val response = likesApi.unLikePost(LongIdentificationWrapper(postId)).awaitResponse()
Log.d("likes_repo", response.toString())
if(response.isSuccessful) {
response.body()?.let { emit(it) }
}
}
}

View File

@ -0,0 +1,3 @@
package com.isolaatti.posting.likes.data.remote
data class LikeDto(val likesCount: Int, val postId: Long)

View File

@ -0,0 +1,14 @@
package com.isolaatti.posting.likes.data.remote
import com.isolaatti.utils.LongIdentificationWrapper
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST
interface LikesApi {
@POST("Likes/LikePost")
fun likePost(@Body postId: LongIdentificationWrapper): Call<LikeDto>
@POST("Likes/UnLikePost")
fun unLikePost(@Body postId: LongIdentificationWrapper): Call<LikeDto>
}

View File

@ -0,0 +1,9 @@
package com.isolaatti.posting.likes.domain.repository
import com.isolaatti.posting.likes.data.remote.LikeDto
import kotlinx.coroutines.flow.Flow
interface LikesRepository {
fun likePost(postId: Long): Flow<LikeDto>
fun unLikePost(postId: Long): Flow<LikeDto>
}

View File

@ -1,7 +1,7 @@
package com.isolaatti.posts.data.remote
package com.isolaatti.posting.posts.data.remote
import com.isolaatti.home.feed.data.remote.FeedDto
import com.isolaatti.home.feed.data.remote.PostDto
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

View File

@ -0,0 +1,8 @@
package com.isolaatti.posting.posts.data.repository
import com.isolaatti.posting.posts.data.remote.PostsApi
import com.isolaatti.posting.posts.domain.PostsRepository
import javax.inject.Inject
class PostsRepositoryImpl @Inject constructor(private val postsApi: PostsApi) : PostsRepository {
}

View File

@ -0,0 +1,4 @@
package com.isolaatti.posting.posts.domain
interface PostsRepository {
}

View File

@ -0,0 +1,4 @@
package com.isolaatti.posting.posts.domain.use_case
class MakePost {
}

View File

@ -0,0 +1,168 @@
package com.isolaatti.posting.posts.presentation
import android.annotation.SuppressLint
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
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.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<PostDto>) : RecyclerView.Adapter<PostsRecyclerViewAdapter.FeedViewHolder>(){
inner class FeedViewHolder(itemView: View) : ViewHolder(itemView) {
fun bindView(postDto: PostDto, payloads: List<Any>) {
Log.d("payloads", payloads.count().toString())
val likeButton: MaterialButton = itemView.findViewById(R.id.like_button)
val commentsButton: MaterialButton = itemView.findViewById(R.id.comment_button)
if(payloads.isNotEmpty()) {
for(payload in payloads) {
when(payload) {
is LikeCountUpdatePayload -> {
likeButton.isEnabled = true
if(postDto.liked) {
likeButton.setIconTintResource(R.color.purple_700)
likeButton.setTextColor(itemView.context.getColor(R.color.purple_700))
} else {
likeButton.setIconTintResource(R.color.black)
likeButton.setTextColor(itemView.context.getColor(R.color.black))
}
likeButton.text = postDto.numberOfLikes.toString()
}
is CommentsCountUpdatePayload -> {
commentsButton.text = postDto.numberOfComments.toString()
}
}
}
} else {
val username: TextView = itemView.findViewById(R.id.text_view_username)
username.text = postDto.userName
val profileImageView: ImageView = itemView.findViewById(R.id.avatar_picture)
Picasso.get().load(userProfileImage(postDto.post.userId)).into(profileImageView)
val dateTextView: TextView = itemView.findViewById(R.id.text_view_date)
dateTextView.text = postDto.post.date
val content: TextView = itemView.findViewById(R.id.post_content)
markwon.setMarkdown(content, postDto.post.textContent)
likeButton.isEnabled = true
if(postDto.liked) {
likeButton.setIconTintResource(R.color.purple_700)
likeButton.setTextColor(itemView.context.getColor(R.color.purple_700))
} else {
likeButton.setIconTintResource(R.color.black)
likeButton.setTextColor(itemView.context.getColor(R.color.black))
}
likeButton.text = postDto.numberOfLikes.toString()
commentsButton.text = postDto.numberOfComments.toString()
val moreButton: MaterialButton = itemView.findViewById(R.id.more_button)
moreButton.setOnClickListener {
callback.onOptions(postDto.post.id)
}
likeButton.setOnClickListener {
likeButton.isEnabled = false
if(postDto.liked){
callback.onUnLiked(postDto.post.id)
} else {
callback.onLiked(postDto.post.id)
}
}
commentsButton.setOnClickListener {
callback.onComment(postDto.post.id)
}
}
}
}
data class LikeCountUpdatePayload(val likeCount: Int)
data class CommentsCountUpdatePayload(val commentsCount: Int)
data class UpdateEvent(val updateType: UpdateType, val affectedId: Long) {
enum class UpdateType {
POST_LIKED,
POST_COMMENTED,
POST_REMOVED,
POST_ADDED
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FeedViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.post_layout, parent, false)
return FeedViewHolder(view)
}
override fun getItemCount(): Int = list.size
override fun getItemId(position: Int): Long = list[position].post.id
override fun setHasStableIds(hasStableIds: Boolean) {
super.setHasStableIds(true)
}
@SuppressLint("NotifyDataSetChanged")
fun updateList(newList: List<PostDto>?, updateEvent: UpdateEvent? = null) {
if(updateEvent == null) {
if(newList != null) {
list = newList
}
notifyDataSetChanged()
return
}
val postUpdated = list.find { p -> p.post.id == updateEvent.affectedId } ?: return
val position = list.indexOf(postUpdated)
if(newList != null) {
list = newList
}
when(updateEvent.updateType) {
UpdateEvent.UpdateType.POST_LIKED -> {
notifyItemChanged(position, LikeCountUpdatePayload(postUpdated.numberOfLikes))
}
UpdateEvent.UpdateType.POST_COMMENTED -> {
notifyItemChanged(position, CommentsCountUpdatePayload(postUpdated.numberOfComments))
}
UpdateEvent.UpdateType.POST_REMOVED -> {
notifyItemRemoved(position)
}
UpdateEvent.UpdateType.POST_ADDED -> {
notifyItemInserted(0)
}
}
}
override fun onBindViewHolder(holder: FeedViewHolder, position: Int) {}
override fun onBindViewHolder(holder: FeedViewHolder, position: Int, payloads: List<Any>) {
holder.bindView(list[position], payloads)
}
}

View File

@ -0,0 +1,89 @@
package com.isolaatti.posting.posts.presentation
import android.util.Log
import androidx.lifecycle.LiveData
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.likes.data.remote.LikeDto
import com.isolaatti.posting.likes.domain.repository.LikesRepository
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 PostsViewModel @Inject constructor(private val feedRepository: FeedRepository, private val likesRepository: LikesRepository) : ViewModel() {
private val _posts: MutableLiveData<FeedDto> = MutableLiveData()
val posts: LiveData<FeedDto> get() = _posts
private val _comments: MutableLiveData<FeedCommentsDto> = MutableLiveData()
val comments: LiveData<FeedCommentsDto> get() = _comments
private fun getLastId(): Long = try {_posts.value?.data?.last()?.post?.id ?: 0} catch (e: NoSuchElementException) { 0 }
private val _postLiked: MutableLiveData<LikeDto> = MutableLiveData()
val postLiked: LiveData<LikeDto> get() = _postLiked
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)
}
temp.let {
_posts.postValue(it)
}
} else {
_posts.postValue(feedDto)
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
fun likePost(postId: Long) {
viewModelScope.launch {
likesRepository.likePost(postId).onEach {likeDto ->
val likedPost = _posts.value?.data?.find { post -> post.post.id == likeDto.postId }
val index = _posts.value?.data?.indexOf(likedPost)
Log.d("***", index.toString())
if(index != null){
val temp = _posts.value
Log.d("***", temp.toString())
temp?.data?.set(index, likedPost!!.apply {
liked = true
numberOfLikes = likeDto.likesCount
})
}
_postLiked.postValue(likeDto)
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
fun unLikePost(postId: Long) {
viewModelScope.launch {
likesRepository.unLikePost(postId).onEach {likeDto ->
val likedPost = _posts.value?.data?.find { post -> post.post.id == likeDto.postId }
val index = _posts.value?.data?.indexOf(likedPost)
if(index != null){
_posts.value?.data?.set(index, likedPost!!.apply {
liked = false
numberOfLikes = likeDto.likesCount
})
}
_postLiked.postValue(likeDto)
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
}

View File

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

View File

@ -1,8 +0,0 @@
package com.isolaatti.posts.data.repository
import com.isolaatti.posts.data.remote.PostsApi
import com.isolaatti.posts.domain.PostsRepository
import javax.inject.Inject
class PostsRepositoryImpl @Inject constructor(private val postsApi: PostsApi) : PostsRepository {
}

View File

@ -1,4 +0,0 @@
package com.isolaatti.posts.domain
interface PostsRepository {
}

View File

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

View File

@ -0,0 +1,4 @@
package com.isolaatti.profile.domain.use_case
class GetProfile {
}

View File

@ -17,6 +17,10 @@ class SettingsFragment : Fragment() {
): View? {
viewBinding = FragmentSettingsBinding.inflate(inflater)
viewBinding.topAppBar.setNavigationOnClickListener {
requireActivity().onBackPressed()
}
return viewBinding.root
}
}

View File

@ -1,3 +1,4 @@
package com.isolaatti.utils
data class IntIdentificationWrapper(val id: Int)
data class LongIdentificationWrapper(val id: Long)

View File

@ -0,0 +1,9 @@
package com.isolaatti.utils
class Resource<T> {
inner class Success(data: T)
inner class Loading()
inner class Error
}

View File

@ -3,5 +3,5 @@ package com.isolaatti.utils
import com.isolaatti.connectivity.RetrofitClient.Companion.BASE_URL
object UrlGen {
fun userProfileImage(userId: Int) = "${BASE_URL}images/profile_image/of_user/1?mode=small"
fun userProfileImage(userId: Int) = "${BASE_URL}images/profile_image/of_user/$userId?mode=small"
}

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="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

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="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>

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="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
</vector>

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="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector>

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="M21,11l0,-8l-8,0l3.29,3.29l-10,10l-3.29,-3.29l0,8l8,0l-3.29,-3.29l10,-10z"/>
</vector>

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="M15.73,3L8.27,3L3,8.27v7.46L8.27,21h7.46L21,15.73L21,8.27L15.73,3zM12,17.3c-0.72,0 -1.3,-0.58 -1.3,-1.3 0,-0.72 0.58,-1.3 1.3,-1.3 0.72,0 1.3,0.58 1.3,1.3 0,0.72 -0.58,1.3 -1.3,1.3zM13,13h-2L11,7h2v6z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" 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="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
</vector>

View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
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">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_drawer"
app:layout_constraintTop_toTopOf="parent"
tools:context=".feed.ui.FeedFragment">
<com.google.android.material.search.SearchBar
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:hint="@string/searchbar_hint" />
<com.google.android.material.search.SearchView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="@string/searchbar_hint"
app:layout_anchor="@id/search_bar">
<!-- Search suggestions/results go here (ScrollView, RecyclerView, etc.). -->
</com.google.android.material.search.SearchView>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="80dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/topAppBar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/topAppBar"
style="@style/Theme.Isolaatti"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/baseline_menu_24"
app:title="@string/app_name"
app:titleCentered="true" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/feed_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floating_action_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription=""
app:srcCompat="@drawable/baseline_add_24" />
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/home_drawer"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/header_drawer_home"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:menu="@menu/home_drawer_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -76,7 +76,7 @@
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/profile_view_pager2"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -6,6 +6,7 @@
<androidx.fragment.app.FragmentContainerView
android:id="@+id/settings_fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/topAppBar_layout"
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"
app:layout_constraintBottom_toTopOf="@id/recycler_comments">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/topAppBar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:titleCentered="true"
style="@style/Theme.Isolaatti"
app:title="Comments"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_comments"
android:layout_width="match_parent"
android:layout_height="400dp"
app:layout_constraintBottom_toTopOf="@id/newCommentTextField"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/topAppBar_layout" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/newCommentTextField"
style="?attr/textInputOutlinedStyle"
android:layout_margin="8dp"
app:boxCornerRadiusBottomEnd="20dp"
app:boxCornerRadiusBottomStart="20dp"
app:boxCornerRadiusTopEnd="20dp"
app:boxCornerRadiusTopStart="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/submitCommentButton"
app:layout_constraintStart_toStartOf="parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/new_comment">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/submitCommentButton"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_margin="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/newCommentTextField"
app:layout_constraintBottom_toBottomOf="parent"
app:icon="@drawable/baseline_send_24"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<LinearLayout
android:id="@+id/options_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/edit"
android:textAlignment="textStart" />
<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/delete"
android:textAlignment="textStart" />
<com.google.android.material.button.MaterialButton
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/report"
android:textAlignment="textStart" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?attr/materialCardViewFilledStyle"
android:layout_margin="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="4dp">
<RelativeLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/avatar_picture"
android:layout_width="40dp"
android:layout_height="40dp"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"/>
<LinearLayout
android:id="@+id/post_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="8dp"
android:layout_toEndOf="@id/avatar_picture"
android:layout_toStartOf="@id/more_button">
<TextView
android:id="@+id/text_view_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textStyle="bold"/>
<TextView
android:id="@+id/text_view_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/more_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?attr/materialIconButtonFilledTonalStyle"
android:layout_alignParentEnd="true"
app:icon="@drawable/baseline_more_horiz_24"
android:gravity="end"/>
</RelativeLayout>
<TextView
android:id="@+id/post_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>

View File

@ -5,13 +5,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/discussionRecycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/discussions"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.drawerlayout.widget.DrawerLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
@ -9,7 +14,7 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".home.feed.ui.FeedFragment">
tools:context=".feed.ui.FeedFragment">
<com.google.android.material.search.SearchBar
android:id="@+id/search_bar"
@ -48,31 +53,15 @@
app:titleCentered="true"
style="@style/Theme.Isolaatti"
app:navigationIcon="@drawable/baseline_menu_24"
app:title="Feed"/>
app:title="@string/app_name"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/feed_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="@id/topAppBar_layout"
app:layout_constraintBottom_toBottomOf="parent"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
android:layout_height="match_parent" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
@ -96,4 +85,5 @@
app:menu="@menu/home_drawer_menu" />
</androidx.drawerlayout.widget.DrawerLayout>
</androidx.drawerlayout.widget.DrawerLayout>
</FrameLayout>

View File

@ -1,16 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/topAppBar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/topAppBar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"/>
android:layout_height="?attr/actionBarSize"
app:title="@string/settings"
app:navigationIcon="@drawable/baseline_arrow_back_24"/>
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:layout_width="match_parent"
@ -19,12 +22,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
<com.google.android.material.button.MaterialButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/account"
android:padding="10dp"
style="?attr/textAppearanceBodyLarge"/>
android:textAlignment="textStart"
style="@style/Widget.MaterialComponents.Button.TextButton"/>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -19,7 +19,7 @@
android:orientation="vertical"
android:padding="4dp">
<LinearLayout android:layout_width="match_parent"
<RelativeLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.imageview.ShapeableImageView
@ -28,11 +28,13 @@
android:layout_height="40dp"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"/>
<LinearLayout
android:layout_width="match_parent"
android:id="@+id/post_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="8dp"
android:layout_gravity="center_vertical">
android:layout_toEndOf="@id/avatar_picture"
android:layout_toStartOf="@id/more_button">
<TextView
android:id="@+id/text_view_username"
android:layout_width="match_parent"
@ -44,30 +46,43 @@
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/more_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?attr/materialIconButtonFilledTonalStyle"
android:layout_alignParentEnd="true"
app:icon="@drawable/baseline_more_horiz_24"
android:gravity="end"/>
</RelativeLayout>
<TextView
android:id="@+id/post_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginHorizontal="8dp"/>
android:layout_marginHorizontal="8dp"
android:layout_marginTop="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="end">
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/comment_button"
style="?attr/materialIconButtonFilledTonalStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/comments_solid" />
<com.google.android.material.button.MaterialButton
android:id="@+id/like_button"
style="?attr/materialIconButtonFilledTonalStyle"
app:icon="@drawable/comments_solid"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?attr/materialIconButtonFilledTonalStyle"
app:icon="@drawable/hands_clapping_solid"/>
app:icon="@drawable/hands_clapping_solid" />
</LinearLayout>
</LinearLayout>

View File

@ -7,7 +7,7 @@
<fragment
android:id="@+id/feedFragment"
android:name="com.isolaatti.home.feed.ui.FeedFragment"
android:name="com.isolaatti.feed.ui.FeedFragment"
android:label="fragment_feed"
tools:layout="@layout/fragment_feed" />
<fragment

View File

@ -11,4 +11,12 @@
<string name="audios">Audios</string>
<string name="images">Images</string>
<string name="account">Account</string>
<string name="settings">Settings</string>
<string name="edit">Edit</string>
<string name="delete">Delete</string>
<string name="new_comment">New comment</string>
<string name="report">Report</string>
<!--Post options -->
<string name="post_options_title">Discussion options</string>
</resources>