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"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="CompilerConfiguration"> <component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" /> <bytecodeTargetLevel target="17" />
</component> </component>
</project> </project>

3
.idea/misc.xml generated
View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <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" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <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 { 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.appcompat:appcompat:1.6.0'
implementation "androidx.recyclerview:recyclerview:1.3.0"
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
@ -66,7 +67,7 @@ dependencies {
implementation "com.google.android.material:material:1.8.0" implementation "com.google.android.material:material:1.8.0"
// Navigation // 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-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version" implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"
@ -81,6 +82,9 @@ dependencies {
// Markwon // Markwon
final def markwon_version = '4.6.2' 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:core:$markwon_version"
implementation "io.noties.markwon:editor:$markwon_version" implementation "io.noties.markwon:editor:$markwon_version"
implementation "io.noties.markwon:image-picasso:$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 com.isolaatti.auth.domain.AuthRepository
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
class AuthenticationInterceptor(private val authRepository: dagger.Lazy<AuthRepository>) : Interceptor { class AuthenticationInterceptor(private val authRepository: dagger.Lazy<AuthRepository>) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
@ -16,7 +15,10 @@ class AuthenticationInterceptor(private val authRepository: dagger.Lazy<AuthRepo
// Add auth header here // Add auth header here
val tokenDto = authRepository.get().getCurrentToken() val tokenDto = authRepository.get().getCurrentToken()
tokenDto?.token?.let { 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) 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 okhttp3.OkHttpClient
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import java.io.IOException
import javax.inject.Inject 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.Call
import retrofit2.http.GET 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.feed.data.remote.FeedApi
import com.isolaatti.home.feed.data.remote.FeedDto import com.isolaatti.feed.data.remote.FeedDto
import com.isolaatti.home.feed.domain.repository.FeedRepository import com.isolaatti.feed.domain.repository.FeedRepository
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import retrofit2.awaitResponse 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( data class Post(
val id: Long, val id: Long,
val textContent: String, var textContent: String,
val userId: Int, val userId: Int,
val privacy: Int, val privacy: Int,
val date: String, val date: String,
val audioId: String, var audioId: String,
val squadId: String, val squadId: String,
val linkedDiscussionId: Long, val linkedDiscussionId: Long,
val linkedCommentId: 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 import kotlinx.coroutines.flow.Flow
interface FeedRepository { interface FeedRepository {

View File

@ -1,8 +1,8 @@
package com.isolaatti.home.feed.ui package com.isolaatti.feed.ui
import android.content.Intent import android.content.Intent
import androidx.lifecycle.ViewModelProvider
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -11,7 +11,11 @@ import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.databinding.FragmentFeedBinding 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.profile.ui.ProfileActivity
import com.isolaatti.settings.ui.SettingsActivity import com.isolaatti.settings.ui.SettingsActivity
import com.isolaatti.utils.PicassoImagesPluginDef import com.isolaatti.utils.PicassoImagesPluginDef
@ -23,15 +27,15 @@ import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAb
import io.noties.markwon.linkify.LinkifyPlugin import io.noties.markwon.linkify.LinkifyPlugin
@AndroidEntryPoint @AndroidEntryPoint
class FeedFragment : Fragment() { class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
companion object { companion object {
fun newInstance() = FeedFragment() fun newInstance() = FeedFragment()
} }
private val viewModel: FeedViewModel by activityViewModels() private val viewModel: PostsViewModel by activityViewModels()
private lateinit var viewBinding: FragmentFeedBinding private lateinit var viewBinding: FragmentFeedBinding
private lateinit var adapter: FeedRecyclerViewAdapter private lateinit var adapter: PostsRecyclerViewAdapter
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
@ -45,9 +49,10 @@ class FeedFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewBinding.topAppBar.setNavigationOnClickListener { viewBinding.topAppBar.setNavigationOnClickListener {
viewBinding.drawerLayout.openDrawer(viewBinding.homeDrawer) viewBinding.drawerLayout?.openDrawer(viewBinding.homeDrawer)
} }
viewBinding.homeDrawer.setNavigationItemSelectedListener { viewBinding.homeDrawer.setNavigationItemSelectedListener {
when(it.itemId) { when(it.itemId) {
R.id.my_profile_menu_item -> { R.id.my_profile_menu_item -> {
@ -73,12 +78,37 @@ class FeedFragment : Fragment() {
.usePlugin(PicassoImagesPluginDef.picassoImagePlugin) .usePlugin(PicassoImagesPluginDef.picassoImagePlugin)
.usePlugin(LinkifyPlugin.create()) .usePlugin(LinkifyPlugin.create())
.build() .build()
adapter = FeedRecyclerViewAdapter(markwon) adapter = PostsRecyclerViewAdapter(markwon, this, listOf())
viewBinding.feedRecyclerView.adapter = adapter viewBinding.feedRecyclerView.adapter = adapter
viewBinding.feedRecyclerView.layoutManager = LinearLayoutManager(requireContext()) 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 package com.isolaatti.followers.domain
import com.isolaatti.profile.data.remote.ProfileListItemDto
import kotlinx.coroutines.flow.Flow
interface FollowersRepository { 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 android.view.Menu
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity 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.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController import androidx.navigation.ui.setupWithNavController
import com.google.android.material.search.SearchBar
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.databinding.ActivityHomeBinding import com.isolaatti.databinding.ActivityHomeBinding
import com.isolaatti.home.feed.presentation.FeedViewModel import com.isolaatti.posting.posts.presentation.PostsViewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
class HomeActivity : AppCompatActivity() { class HomeActivity : AppCompatActivity() {
lateinit var viewBinding: ActivityHomeBinding lateinit var viewBinding: ActivityHomeBinding
val feedViewModel: FeedViewModel by viewModels() val postsViewModel: PostsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -28,7 +23,10 @@ class HomeActivity : AppCompatActivity() {
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
viewBinding.bottomNavigation.setupWithNavController(navHostFragment.navController) viewBinding.bottomNavigation.setupWithNavController(navHostFragment.navController)
feedViewModel.getFeed() if(savedInstanceState == null) {
postsViewModel.getFeed()
}
} }
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {

View File

@ -1,9 +1,9 @@
package com.isolaatti.home package com.isolaatti.home
import com.isolaatti.connectivity.RetrofitClient import com.isolaatti.connectivity.RetrofitClient
import com.isolaatti.home.feed.data.remote.FeedApi import com.isolaatti.feed.data.remote.FeedApi
import com.isolaatti.home.feed.data.repository.FeedRepositoryImpl import com.isolaatti.feed.data.repository.FeedRepositoryImpl
import com.isolaatti.home.feed.domain.repository.FeedRepository import com.isolaatti.feed.domain.repository.FeedRepository
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn 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.connectivity.RetrofitClient
import com.isolaatti.posts.data.remote.PostsApi import com.isolaatti.posting.posts.data.remote.PostsApi
import com.isolaatti.posts.data.repository.PostsRepositoryImpl import com.isolaatti.posting.posts.data.repository.PostsRepositoryImpl
import com.isolaatti.posts.domain.PostsRepository import com.isolaatti.posting.posts.domain.PostsRepository
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn

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.posting.comments.data.remote.CommentsApi
import com.isolaatti.comments.data.repository.CommentsRepositoryImpl import com.isolaatti.posting.comments.data.repository.CommentsRepositoryImpl
import com.isolaatti.comments.domain.CommentsRepository import com.isolaatti.posting.comments.domain.CommentsRepository
import com.isolaatti.connectivity.RetrofitClient import com.isolaatti.connectivity.RetrofitClient
import dagger.Module import dagger.Module
import dagger.Provides 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( data class CommentToPostDto(
val content: String, 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( data class Comment(
val id: Long, 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.feed.data.remote.FeedDto
import com.isolaatti.home.feed.data.remote.PostDto import com.isolaatti.feed.data.remote.PostDto
import com.isolaatti.profile.data.remote.ProfileListItemDto import com.isolaatti.profile.data.remote.ProfileListItemDto
import retrofit2.Call import retrofit2.Call
import retrofit2.http.GET 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 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.Call
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Path import retrofit2.http.Path
@ -10,5 +10,4 @@ interface ProfileApi {
@GET("Fetch/UserProfile/{userId}") @GET("Fetch/UserProfile/{userId}")
fun userProfile(@Path("userId") userId: Int): Call<UserProfileDto> 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? { ): View? {
viewBinding = FragmentSettingsBinding.inflate(inflater) viewBinding = FragmentSettingsBinding.inflate(inflater)
viewBinding.topAppBar.setNavigationOnClickListener {
requireActivity().onBackPressed()
}
return viewBinding.root return viewBinding.root
} }
} }

View File

@ -1,3 +1,4 @@
package com.isolaatti.utils package com.isolaatti.utils
data class IntIdentificationWrapper(val id: Int) 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 import com.isolaatti.connectivity.RetrofitClient.Companion.BASE_URL
object UrlGen { 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

@ -6,6 +6,7 @@
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/settings_fragment_container" android:id="@+id/settings_fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:defaultNavHost="true" 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_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<TextView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/textView" android:id="@+id/discussionRecycler"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/discussions" app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout" android:id="@+id/drawer_layout"
@ -9,7 +14,7 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".home.feed.ui.FeedFragment"> tools:context=".feed.ui.FeedFragment">
<com.google.android.material.search.SearchBar <com.google.android.material.search.SearchBar
android:id="@+id/search_bar" android:id="@+id/search_bar"
@ -48,31 +53,15 @@
app:titleCentered="true" app:titleCentered="true"
style="@style/Theme.Isolaatti" style="@style/Theme.Isolaatti"
app:navigationIcon="@drawable/baseline_menu_24" app:navigationIcon="@drawable/baseline_menu_24"
app:title="Feed"/> app:title="@string/app_name"/>
</com.google.android.material.appbar.AppBarLayout> </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 <androidx.recyclerview.widget.RecyclerView
android:id="@+id/feed_recycler_view" android:id="@+id/feed_recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent" />
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>
</LinearLayout> </LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
@ -97,3 +86,4 @@
</androidx.drawerlayout.widget.DrawerLayout> </androidx.drawerlayout.widget.DrawerLayout>
</FrameLayout>

View File

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

View File

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

View File

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

View File

@ -11,4 +11,12 @@
<string name="audios">Audios</string> <string name="audios">Audios</string>
<string name="images">Images</string> <string name="images">Images</string>
<string name="account">Account</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> </resources>