Merge remote-tracking branch 'origin/dev' into dev

# Conflicts:
#	app/src/main/java/com/isolaatti/posting/posts/ui/PostListingFragment.kt
#	app/src/main/res/values/strings.xml
This commit is contained in:
erik-everardo 2024-04-14 02:22:04 -06:00
commit ce8ec23100
44 changed files with 883 additions and 319 deletions

145
.idea/navEditor.xml generated
View File

@ -59,11 +59,66 @@
</LayoutPositions>
</value>
</entry>
<entry key="hashtags_navigation.xml">
<value>
<LayoutPositions>
<option name="myPositions">
<map>
<entry key="hashtagPostsFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-3" />
<option name="y" value="36" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="hashtagsFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-219" />
<option name="y" value="36" />
</Point>
</option>
<option name="myPositions">
<map>
<entry key="action_hashtagsFragment_to_hashtagPostsFragment">
<value>
<LayoutPositions />
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="home_navigation.xml">
<value>
<LayoutPositions>
<option name="myPositions">
<map>
<entry key="browseProfilesFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="45" />
<option name="y" value="385" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="feedFragment">
<value>
<LayoutPositions>
@ -85,6 +140,39 @@
</LayoutPositions>
</value>
</entry>
<entry key="hashtagPostsFragment2">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="267" />
<option name="y" value="107" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="hashtagsFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="36" />
<option name="y" value="-76" />
</Point>
</option>
<option name="myPositions">
<map>
<entry key="action_hashtagsFragment2_to_hashtagPostsFragment2">
<value>
<LayoutPositions />
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="notificationsFragment">
<value>
<LayoutPositions>
@ -106,18 +194,6 @@
</LayoutPositions>
</value>
</entry>
<entry key="postViewerActivity">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-520" />
<option name="y" value="-373" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="profileActivity">
<value>
<LayoutPositions>
@ -135,10 +211,29 @@
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="12" />
<option name="y" value="12" />
<option name="x" value="-278" />
<option name="y" value="216" />
</Point>
</option>
<option name="myPositions">
<map>
<entry key="action_searchFragment_to_browseProfilesFragment">
<value>
<LayoutPositions />
</value>
</entry>
<entry key="action_searchFragment_to_hashtagPostsFragment">
<value>
<LayoutPositions />
</value>
</entry>
<entry key="action_searchFragment_to_hashtagsFragment">
<value>
<LayoutPositions />
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>
@ -246,6 +341,28 @@
</LayoutPositions>
</value>
</entry>
<entry key="post_listing_navigation.xml">
<value>
<LayoutPositions>
<option name="myPositions">
<map>
<entry key="postListingFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="40" />
<option name="y" value="40" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="profile_navigation.xml">
<value>
<LayoutPositions>

View File

@ -22,8 +22,8 @@ android {
applicationId "com.isolaatti"
minSdk 24
targetSdk 34
versionCode 2
versionName "0.2"
versionCode 3
versionName "0.3-vc3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

@ -26,15 +26,15 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".home.HomeActivity" android:theme="@style/Theme.Isolaatti" />
<activity android:name=".home.ui.HomeActivity" android:theme="@style/Theme.Isolaatti" />
<activity android:name=".login.LogInActivity" android:theme="@style/Theme.Isolaatti" />
<activity android:name=".profile.ui.ProfileActivity"
android:theme="@style/Theme.Isolaatti"
android:parentActivityName=".home.HomeActivity"/>
android:parentActivityName=".home.ui.HomeActivity"/>
<activity android:name=".settings.ui.SettingsActivity" android:theme="@style/Theme.Isolaatti"/>
<activity android:name=".posting.posts.ui.CreatePostActivity" android:theme="@style/Theme.Isolaatti" android:windowSoftInputMode="adjustResize"/>
<activity android:name=".posting.posts.viewer.ui.PostViewerActivity" android:theme="@style/Theme.Isolaatti"
android:parentActivityName=".home.HomeActivity"/>
android:parentActivityName=".home.ui.HomeActivity"/>
<activity android:name=".drafts.ui.DraftsActivity" android:theme="@style/Theme.Isolaatti"/>
<activity android:name=".about.AboutActivity" android:theme="@style/Theme.Isolaatti"/>
<activity android:name=".images.picture_viewer.ui.PictureViewerActivity" android:theme="@style/Theme.Isolaatti"/>

View File

@ -7,7 +7,7 @@ import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.isolaatti.auth.data.AuthRepositoryImpl
import com.isolaatti.home.HomeActivity
import com.isolaatti.home.ui.HomeActivity
import com.isolaatti.login.LogInActivity
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

View File

@ -5,6 +5,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.net.toUri
import com.isolaatti.BuildConfig
import com.isolaatti.R
import com.isolaatti.databinding.ActivityAboutBinding
class AboutActivity : AppCompatActivity() {
@ -51,5 +52,7 @@ class AboutActivity : AppCompatActivity() {
.build()
.launchUrl(this, BuildConfig.terms.toUri())
}
binding.appVersion.text = getString(R.string.app_version, BuildConfig.VERSION_NAME)
}
}

View File

@ -6,7 +6,6 @@ import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
@ -14,9 +13,7 @@ import androidx.lifecycle.Observer
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.isolaatti.R
import com.isolaatti.connectivity.ConnectivityCallbackImpl
import com.isolaatti.connectivity.NetworkStatus
import com.isolaatti.home.HomeActivity
import com.isolaatti.login.LogInActivity
import com.isolaatti.utils.Resource
import dagger.hilt.android.AndroidEntryPoint

View File

@ -26,6 +26,12 @@ class RetrofitClient @Inject constructor(private val authenticationInterceptor:
private val okHttpClient get() = OkHttpClient.Builder()
.addInterceptor(authenticationInterceptor)
.addInterceptor { chain ->
chain.proceed(chain.request()
.newBuilder()
.header("User-Agent", "Isolaatti Android ${BuildConfig.VERSION_NAME}")
.build())
}
.connectTimeout(5, TimeUnit.MINUTES)
.writeTimeout(5, TimeUnit.MINUTES)
.readTimeout(5, TimeUnit.MINUTES)

View File

@ -0,0 +1,12 @@
package com.isolaatti.hashtags.data
import com.isolaatti.common.ResultDto
import com.isolaatti.posting.posts.data.remote.FeedDto
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
interface HashtagsApi {
}

View File

@ -0,0 +1,46 @@
package com.isolaatti.hashtags.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import com.isolaatti.databinding.FragmentPostsHashtagBinding
import androidx.navigation.fragment.navArgs
import com.isolaatti.R
import com.isolaatti.posting.posts.ui.PostListingFragment
class HashtagPostsFragment : Fragment() {
private lateinit var binding: FragmentPostsHashtagBinding
private val navArgs: HashtagPostsFragmentArgs by navArgs()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentPostsHashtagBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.title = "#${navArgs.hashtag}"
setupListeners()
// show feed
(childFragmentManager.findFragmentById(R.id.post_list_fragment_container) as NavHostFragment)
.navController.setGraph(R.navigation.post_listing_navigation, Bundle().apply { putString(PostListingFragment.ARG_HASHTAG, navArgs.hashtag) })
}
private fun setupListeners() {
binding.toolbar.setNavigationOnClickListener {
findNavController().popBackStack()
}
}
}

View File

@ -1,12 +0,0 @@
package com.isolaatti.hashtags.ui
import android.content.Context
import com.isolaatti.common.IsolaattiBaseActivity
class HashtagsActivity : IsolaattiBaseActivity() {
companion object {
fun startActivity(context: Context, hashtag: String? = null) {
}
}
}

View File

@ -0,0 +1,35 @@
package com.isolaatti.hashtags.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.isolaatti.databinding.FragmentHashtagsBinding
class HashtagsFragment : Fragment() {
private lateinit var binding: FragmentHashtagsBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentHashtagsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupListeners()
}
private fun setupListeners() {
binding.toolbar.setNavigationOnClickListener {
findNavController().popBackStack()
}
}
}

View File

@ -2,12 +2,12 @@ package com.isolaatti.home.presentation
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.isolaatti.auth.domain.AuthRepository
import com.isolaatti.posting.posts.domain.PostsRepository
import com.isolaatti.posting.posts.presentation.PostListingViewModelBase
import com.isolaatti.posting.posts.presentation.UpdateEvent
import com.isolaatti.profile.data.remote.UserProfileDto
import com.isolaatti.profile.domain.entity.UserProfile
import com.isolaatti.profile.domain.use_case.GetProfile
import com.isolaatti.utils.Resource
@ -22,9 +22,8 @@ import javax.inject.Inject
@HiltViewModel
class FeedViewModel @Inject constructor(
private val getProfileUseCase: GetProfile,
private val authRepository: AuthRepository,
private val postsRepository: PostsRepository
) : PostListingViewModelBase() {
private val authRepository: AuthRepository
) : ViewModel() {
private val toRetry: MutableList<Runnable> = mutableListOf()
@ -39,41 +38,7 @@ class FeedViewModel @Inject constructor(
toRetry.clear()
}
override fun getFeed(refresh: Boolean) {
viewModelScope.launch {
if (refresh) {
posts.value = null
}
postsRepository.getFeed(getLastId()).onEach { listResource ->
when (listResource) {
is Resource.Success -> {
val eventType = if((postsList?.size ?: 0) > 0) UpdateEvent.UpdateType.PAGE_ADDED else UpdateEvent.UpdateType.REFRESH
loadingPosts.postValue(false)
posts.postValue(Pair(postsList?.apply {
addAll(listResource.data ?: listOf())
} ?: listResource.data,
UpdateEvent(eventType, null)))
noMoreContent.postValue(listResource.data?.size == 0)
}
is Resource.Loading -> {
if(!refresh)
loadingPosts.postValue(true)
}
is Resource.Error -> {
errorLoading.postValue(listResource.errorType)
toRetry.add {
getFeed(refresh)
}
}
}
isLoadingFromScrolling = false
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
// User profile
private val _userProfile: MutableLiveData<UserProfile> = MutableLiveData()
@ -88,7 +53,6 @@ class FeedViewModel @Inject constructor(
when(profile) {
is Resource.Error -> {
errorLoading.postValue(profile.errorType)
toRetry.add {
getProfile()
}

View File

@ -0,0 +1,140 @@
package com.isolaatti.home.ui
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import coil.load
import com.google.android.material.card.MaterialCardView
import com.isolaatti.R
import com.isolaatti.about.AboutActivity
import com.isolaatti.common.CoilImageLoader.imageLoader
import com.isolaatti.common.ErrorMessageViewModel
import com.isolaatti.databinding.FragmentFeedBinding
import com.isolaatti.home.presentation.FeedViewModel
import com.isolaatti.posting.posts.presentation.CreatePostContract
import com.isolaatti.posting.posts.ui.PostListingFragment
import com.isolaatti.profile.ui.ProfileActivity
import com.isolaatti.settings.ui.SettingsActivity
import com.isolaatti.utils.UrlGen
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@AndroidEntryPoint
class FeedFragment : Fragment() {
companion object {
fun newInstance() = FeedFragment()
const val CALLER_ID = 20
}
private val errorViewModel: ErrorMessageViewModel by activityViewModels()
private val viewModel: FeedViewModel by activityViewModels()
private var currentUserId = 0
private lateinit var viewBinding: FragmentFeedBinding
private val createDiscussion = registerForActivityResult(CreatePostContract()) {
if(it != null) {
Toast.makeText(requireContext(), R.string.posted_successfully, Toast.LENGTH_SHORT).show()
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewBinding = FragmentFeedBinding.inflate(inflater)
return viewBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewBinding.topAppBar.setNavigationOnClickListener {
viewBinding.drawerLayout?.openDrawer(viewBinding.homeDrawer)
}
viewBinding.homeDrawer.setNavigationItemSelectedListener {
when(it.itemId) {
R.id.settings_menu_item -> {
startActivity(Intent(requireActivity(), SettingsActivity::class.java))
true
}
R.id.about_menu_item -> {
startActivity(Intent(requireActivity(), AboutActivity::class.java))
true
}
else -> {true}
}
}
viewBinding.topAppBar.setOnMenuItemClickListener {
when(it.itemId) {
R.id.menu_item_new_discussion -> {
createDiscussion.launch(Unit)
true
}
else -> {
false
}
}
}
// show feed
(childFragmentManager.findFragmentById(R.id.post_list_fragment_container) as NavHostFragment).navController.setGraph(R.navigation.post_listing_navigation)
viewModel.userProfile.observe(viewLifecycleOwner) {
val header = viewBinding.homeDrawer.getHeaderView(0) as? ConstraintLayout
val image: ImageView? = header?.findViewById(R.id.profileImageView)
val textViewName: TextView? = header?.findViewById(R.id.textViewName)
val textViewEmail: TextView? = header?.findViewById(R.id.textViewEmail)
val textViewUsername: TextView? = header?.findViewById(R.id.textViewUsername)
image?.load(UrlGen.userProfileImage(it.userId), imageLoader)
val card: MaterialCardView? = header?.findViewById(R.id.drawer_header_card)
card?.setOnClickListener {
ProfileActivity.startActivity(requireContext(), currentUserId)
}
textViewName?.text = it.name
textViewEmail?.text = it.email
textViewUsername?.text = "@${it.uniqueUsername}"
currentUserId = it.userId
}
viewModel.getProfile()
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
errorViewModel.retry.collect {
viewModel.retry()
errorViewModel.handleRetry()
}
}
}
}
}

View File

@ -1,4 +1,4 @@
package com.isolaatti.home
package com.isolaatti.home.ui
import android.Manifest
import android.content.SharedPreferences
@ -45,10 +45,6 @@ class HomeActivity : IsolaattiBaseActivity() {
askNotificationPermission()
if(savedInstanceState == null) {
feedViewModel.getFeed(false)
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {

View File

@ -1,5 +1,6 @@
package com.isolaatti.posting.posts.data.remote
import com.isolaatti.common.ResultDto
import com.isolaatti.profile.data.remote.ProfileListItemDto
import retrofit2.Call
import retrofit2.http.GET
@ -22,4 +23,7 @@ interface FeedsApi {
@GET("Feed")
fun getChronology(@Query("lastId") lastId: Long, @Query("length") length: Int): Call<FeedDto>
@GET("hashtags/hashtag/{hashtag}")
fun getHashtagPosts(@Path("hashtag") hashtag: String, @Query("after") afterPost: Long? = null): Call<FeedDto>
}

View File

@ -1,5 +1,6 @@
package com.isolaatti.posting.posts.data.repository
import android.util.Log
import com.google.gson.Gson
import com.isolaatti.posting.posts.data.remote.CreatePostDto
import com.isolaatti.posting.posts.data.remote.DeletePostDto
@ -18,10 +19,18 @@ import retrofit2.awaitResponse
import javax.inject.Inject
class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, private val postApi: PostApi) : PostsRepository {
override fun getFeed(lastId: Long): Flow<Resource<MutableList<Post>>> = flow {
companion object {
const val LOG_TAG = "PostsRepositoryImpl"
}
override fun getFeed(lastId: Long, hashtag: String?): Flow<Resource<MutableList<Post>>> = flow {
emit(Resource.Loading())
try {
val result = feedsApi.getChronology(lastId, 20).execute()
val result = if(hashtag == null) {
feedsApi.getChronology(lastId, 20).awaitResponse()
} else {
feedsApi.getHashtagPosts(hashtag, lastId).awaitResponse()
}
if(result.isSuccessful) {
emit(Resource.Success(result.body()?.let { Post.fromFeedDto(it) }))
return@flow
@ -32,7 +41,8 @@ class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, pr
500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError))
else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError))
}
} catch(_: Exception) {
} catch(e: Exception) {
Log.e(LOG_TAG, "Error $e")
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
}

View File

@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.Flow
interface PostsRepository {
fun getFeed(lastId: Long): Flow<Resource<MutableList<Post>>>
fun getFeed(lastId: Long, hashtag: String?): Flow<Resource<MutableList<Post>>>
fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto?): Flow<Resource<MutableList<Post>>>

View File

@ -0,0 +1,48 @@
package com.isolaatti.posting.posts.presentation
import androidx.lifecycle.viewModelScope
import com.isolaatti.posting.posts.domain.PostsRepository
import com.isolaatti.utils.Resource
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class PostListingViewModel @Inject constructor(private val postsRepository: PostsRepository) : PostListingViewModelBase() {
override fun getFeed(refresh: Boolean, hashtag: String?) {
viewModelScope.launch {
if (refresh) {
posts.value = null
}
postsRepository.getFeed(getLastId(), hashtag).onEach { listResource ->
when (listResource) {
is Resource.Success -> {
val eventType = if((postsList?.size ?: 0) > 0) UpdateEvent.UpdateType.PAGE_ADDED else UpdateEvent.UpdateType.REFRESH
loadingPosts.postValue(false)
posts.postValue(Pair(postsList?.apply {
addAll(listResource.data ?: listOf())
} ?: listResource.data,
UpdateEvent(eventType, null)))
noMoreContent.postValue(listResource.data?.size == 0)
}
is Resource.Loading -> {
if(!refresh)
loadingPosts.postValue(true)
}
is Resource.Error -> {
errorLoading.postValue(listResource.errorType)
}
}
isLoadingFromScrolling = false
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
}

View File

@ -44,7 +44,7 @@ abstract class PostListingViewModelBase : ViewModel() {
fun getLastId(): Long = try { posts.value?.first?.last()?.id ?: 0 } catch (e: NoSuchElementException) { 0 }
abstract fun getFeed(refresh: Boolean)
abstract fun getFeed(refresh: Boolean, hashtag: String?)
fun likePost(postId: Long) {
viewModelScope.launch {

View File

@ -361,7 +361,9 @@ class PostsRecyclerViewAdapter (
if(totalItems != null && totalItems > 0 && !requestedNewContent) {
if(position == totalItems - 1) {
requestedNewContent = true
callback.onLoadMore()
if(payloads.isEmpty()) {
callback.onLoadMore()
}
}
}

View File

@ -1,53 +1,38 @@
package com.isolaatti.home
package com.isolaatti.posting.posts.ui
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
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 android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import coil.load
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.isolaatti.BuildConfig
import com.isolaatti.R
import com.isolaatti.about.AboutActivity
import com.isolaatti.audio.common.domain.Audio
import com.isolaatti.audio.common.domain.Playable
import com.isolaatti.audio.player.AudioPlayerConnector
import com.isolaatti.common.CoilImageLoader.imageLoader
import com.isolaatti.common.CoilImageLoader
import com.isolaatti.common.Dialogs
import com.isolaatti.common.ErrorMessageViewModel
import com.isolaatti.databinding.FragmentFeedBinding
import com.isolaatti.drafts.ui.DraftsActivity
import com.isolaatti.home.presentation.FeedViewModel
import com.isolaatti.images.picture_viewer.ui.PictureViewerActivity
import com.isolaatti.posting.posts.viewer.ui.PostViewerActivity
import com.isolaatti.posting.comments.ui.BottomSheetPostComments
import com.isolaatti.common.OnUserInteractedWithPostCallback
import com.isolaatti.common.Ownable
import com.isolaatti.common.options_bottom_sheet.domain.OptionClicked
import com.isolaatti.common.options_bottom_sheet.domain.Options
import com.isolaatti.common.options_bottom_sheet.presentation.BottomSheetPostOptionsViewModel
import com.isolaatti.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment
import com.isolaatti.databinding.FragmentPostListingBinding
import com.isolaatti.home.presentation.FeedViewModel
import com.isolaatti.home.ui.FeedFragment
import com.isolaatti.posting.comments.ui.BottomSheetPostComments
import com.isolaatti.posting.posts.domain.entity.Post
import com.isolaatti.posting.posts.presentation.CreatePostContract
import com.isolaatti.posting.posts.presentation.EditPostContract
import com.isolaatti.posting.posts.presentation.PostListingViewModel
import com.isolaatti.posting.posts.presentation.PostsRecyclerViewAdapter
import com.isolaatti.posting.posts.viewer.ui.PostViewerActivity
import com.isolaatti.profile.ui.ProfileActivity
import com.isolaatti.reports.ui.NewReportBottomSheetDialogFragment
import com.isolaatti.settings.ui.SettingsActivity
import com.isolaatti.utils.UrlGen
import dagger.hilt.android.AndroidEntryPoint
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
@ -55,85 +40,23 @@ import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.image.coil.CoilImagesPlugin
import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute
import io.noties.markwon.linkify.LinkifyPlugin
import kotlinx.coroutines.launch
import org.w3c.dom.Text
@AndroidEntryPoint
class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
class PostListingFragment : Fragment(), OnUserInteractedWithPostCallback {
companion object {
fun newInstance() = FeedFragment()
const val CALLER_ID = 20
const val ARG_HASHTAG = "hashtag"
const val LOG_TAG = "PostListingFragment"
}
var hashtag: String? = null
private lateinit var viewBinding: FragmentPostListingBinding
private val errorViewModel: ErrorMessageViewModel by activityViewModels()
private val viewModel: FeedViewModel by activityViewModels()
private val viewModel: PostListingViewModel by viewModels()
val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels()
private var currentUserId = 0
private lateinit var viewBinding: FragmentFeedBinding
private lateinit var adapter: PostsRecyclerViewAdapter
private lateinit var audioPlayerConnector: AudioPlayerConnector
// region launchers
private val createDiscussion = registerForActivityResult(CreatePostContract()) {
if(it != null) {
Toast.makeText(requireContext(), R.string.posted_successfully, Toast.LENGTH_SHORT).show()
}
}
private val editDiscussion = registerForActivityResult(EditPostContract()) {
if(it != null) {
viewModel.onPostUpdate(it)
}
}
// endregion
// region observers
private val optionsObserver: Observer<OptionClicked?> = Observer { optionClicked ->
if(optionClicked?.callerId == CALLER_ID) {
// post id should come as payload
val post = optionClicked.payload as? Post ?: return@Observer
when(optionClicked.optionId) {
Options.Option.OPTION_DELETE -> {
Dialogs.buildDeletePostDialog(requireContext()) { delete ->
optionsViewModel.handle()
if(delete) {
viewModel.deletePost(post.id)
}
}.show()
}
Options.Option.OPTION_EDIT -> {
optionsViewModel.handle()
editDiscussion.launch(post.id)
}
Options.Option.OPTION_REPORT -> {
optionsViewModel.handle()
NewReportBottomSheetDialogFragment.newInstance().show(childFragmentManager, NewReportBottomSheetDialogFragment.LOG_TAG)
}
}
}
}
// endregion
// region lifecycle
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewBinding = FragmentFeedBinding.inflate(inflater)
return viewBinding.root
}
// endregion
private val audioPlayerConnectorListener = object: AudioPlayerConnector.Listener {
override fun onPlaying(isPlaying: Boolean, audio: Playable) {
if(audio is Audio)
@ -162,27 +85,24 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
}
// region events
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(LOG_TAG, "onCreate ; hashtag: $hashtag")
hashtag = arguments?.getString(ARG_HASHTAG)
viewModel.getFeed(false, hashtag)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
viewBinding = FragmentPostListingBinding.inflate(inflater, container, false)
return viewBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewBinding.topAppBar.setNavigationOnClickListener {
viewBinding.drawerLayout?.openDrawer(viewBinding.homeDrawer)
}
viewBinding.homeDrawer.setNavigationItemSelectedListener {
when(it.itemId) {
R.id.settings_menu_item -> {
startActivity(Intent(requireActivity(), SettingsActivity::class.java))
true
}
R.id.about_menu_item -> {
startActivity(Intent(requireActivity(), AboutActivity::class.java))
true
}
else -> {true}
}
}
audioPlayerConnector = AudioPlayerConnector(requireContext())
audioPlayerConnector.addListener(audioPlayerConnectorListener)
@ -192,11 +112,12 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
.usePlugin(object: AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder
.imageDestinationProcessor(ImageDestinationProcessorRelativeToAbsolute
.imageDestinationProcessor(
ImageDestinationProcessorRelativeToAbsolute
.create(BuildConfig.backend))
}
})
.usePlugin(CoilImagesPlugin.create(requireContext(), imageLoader))
.usePlugin(CoilImagesPlugin.create(requireContext(), CoilImageLoader.imageLoader))
.usePlugin(LinkifyPlugin.create())
.build()
adapter = PostsRecyclerViewAdapter(markwon, this)
@ -206,44 +127,9 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
viewBinding.swipeToRefresh.setOnRefreshListener {
viewModel.getFeed(refresh = true)
viewModel.getFeed(refresh = true, hashtag)
}
viewBinding.topAppBar.setOnMenuItemClickListener {
when(it.itemId) {
R.id.menu_item_new_discussion -> {
createDiscussion.launch(Unit)
true
}
else -> {
false
}
}
}
viewModel.userProfile.observe(viewLifecycleOwner) {
val header = viewBinding.homeDrawer.getHeaderView(0) as? ConstraintLayout
val image: ImageView? = header?.findViewById(R.id.profileImageView)
val textViewName: TextView? = header?.findViewById(R.id.textViewName)
val textViewEmail: TextView? = header?.findViewById(R.id.textViewEmail)
val textViewUsername: TextView? = header?.findViewById(R.id.textViewUsername)
image?.load(UrlGen.userProfileImage(it.userId), imageLoader)
val card: MaterialCardView? = header?.findViewById(R.id.drawer_header_card)
card?.setOnClickListener {
ProfileActivity.startActivity(requireContext(), currentUserId)
}
textViewName?.text = it.name
textViewEmail?.text = it.email
textViewUsername?.text = "@${it.uniqueUsername}"
currentUserId = it.userId
}
viewModel.posts.observe(viewLifecycleOwner){
if (it?.first != null) {
adapter.updateList(it.first!!, it.second)
@ -263,18 +149,7 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
errorViewModel.error.postValue(it)
}
viewModel.getProfile()
optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
errorViewModel.retry.collect {
viewModel.retry()
errorViewModel.handleRetry()
}
}
}
}
override fun onLiked(postId: Long) = viewModel.likePost(postId)
@ -282,7 +157,7 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
override fun onUnLiked(postId: Long) = viewModel.unLikePost(postId)
override fun onOptions(post: Ownable) {
optionsViewModel.setOptions(Options.POST_OPTIONS, CALLER_ID, post)
optionsViewModel.setOptions(Options.POST_OPTIONS, FeedFragment.CALLER_ID, post)
val modalBottomSheet = BottomSheetPostOptionsFragment()
modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostOptionsFragment.TAG)
}
@ -305,8 +180,39 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
}
override fun onLoadMore() {
viewModel.getFeed(false)
viewModel.getFeed(false, hashtag)
}
// endregion
private val editDiscussion = registerForActivityResult(EditPostContract()) {
if(it != null) {
viewModel.onPostUpdate(it)
}
}
private val optionsObserver: Observer<OptionClicked?> = Observer { optionClicked ->
if(optionClicked?.callerId == FeedFragment.CALLER_ID) {
// post id should come as payload
val post = optionClicked.payload as? Post ?: return@Observer
when(optionClicked.optionId) {
Options.Option.OPTION_DELETE -> {
Dialogs.buildDeletePostDialog(requireContext()) { delete ->
optionsViewModel.handle()
if(delete) {
viewModel.deletePost(post.id)
}
}.show()
}
Options.Option.OPTION_EDIT -> {
optionsViewModel.handle()
editDiscussion.launch(post.id)
}
Options.Option.OPTION_REPORT -> {
optionsViewModel.handle()
}
}
}
}
}

View File

@ -111,7 +111,7 @@ class ProfileViewModel @Inject constructor(
}
}
override fun getFeed(refresh: Boolean) {
override fun getFeed(refresh: Boolean, hashtag: String?) {
viewModelScope.launch {
if(refresh) {
posts.value = Pair(null, UpdateEvent(UpdateEvent.UpdateType.REFRESH, null))
@ -132,7 +132,7 @@ class ProfileViewModel @Inject constructor(
is Resource.Error -> {
errorLoading.postValue(feedDtoResource.errorType)
toRetry.add {
getFeed(refresh)
getFeed(refresh, hashtag)
}
}
}

View File

@ -0,0 +1,6 @@
package com.isolaatti.profile.ui
import androidx.fragment.app.Fragment
class BrowseProfilesFragment : Fragment() {
}

View File

@ -360,7 +360,7 @@ class ProfileMainFragment : Fragment() {
viewBinding.feedRecyclerView.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
viewBinding.swipeToRefresh.setOnRefreshListener {
viewModel.getFeed(true)
viewModel.getFeed(true, null)
}
viewBinding.playButton.setOnClickListener {
audioDescriptionAudio?.let { audio ->
@ -398,7 +398,7 @@ class ProfileMainFragment : Fragment() {
userId?.let { profileId ->
viewModel.profileId = profileId
viewModel.getProfile()
viewModel.getFeed(true)
viewModel.getFeed(true, null)
}
}
@ -468,7 +468,7 @@ class ProfileMainFragment : Fragment() {
}
override fun onLoadMore() {
viewModel.getFeed(false)
viewModel.getFeed(false, null)
}
}
}

View File

@ -5,8 +5,8 @@ import retrofit2.http.GET
import retrofit2.http.Query
interface SearchApi {
@GET("Search/Quick")
fun quickSearch(@Query("q") query: String): Call<SearchDto>
@GET("Search/v2")
fun quickSearch(@Query("query") query: String): Call<SearchDto>
@GET("hashtags/trending")
fun getTrendingHashtags(): Call<HashtagsDto>

View File

@ -1,14 +1,33 @@
package com.isolaatti.search.data
import com.isolaatti.posting.posts.data.remote.FeedDto
enum class SearchResultType {
Profile, Post, Hashtag, Unknown
}
data class ProfileSearchDto(val id: Int, val name: String, val imageId: String?, val following: Boolean)
data class SearchDto(
val profiles: List<ProfileSearchDto>,
val posts: List<FeedDto.PostDto>
// TODO add the other types
)
data class SearchDto(val result: List<SearchResultDto>)
data class SearchResultDto(
val origin: String,
val resourceId: String,
val title: String,
val description: String
) {
companion object {
const val ORIGIN_POSTS = "posts"
const val ORIGIN_HASHTAGS = "hashtags"
const val ORIGIN_USERS = "users"
}
val type: SearchResultType get() {
return when(origin) {
ORIGIN_POSTS -> SearchResultType.Post
ORIGIN_HASHTAGS -> SearchResultType.Hashtag
ORIGIN_USERS -> SearchResultType.Profile
else -> SearchResultType.Unknown
}
}
}
data class HashtagsDto(val result: List<String>)

View File

@ -0,0 +1,61 @@
package com.isolaatti.search.presentation
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import coil.load
import com.isolaatti.R
import com.isolaatti.databinding.SearchResultItemBinding
import com.isolaatti.search.data.SearchResultDto
import com.isolaatti.search.data.SearchResultType
import com.isolaatti.utils.UrlGen
class SearchResultsAdapter(
private val onItemClick: (item: SearchResultDto ) -> Unit = {}
) : ListAdapter<SearchResultDto, SearchResultsAdapter.SearchResultViewHolder>(itemCallback) {
companion object {
val itemCallback = object: DiffUtil.ItemCallback<SearchResultDto>() {
override fun areItemsTheSame(oldItem: SearchResultDto, newItem: SearchResultDto): Boolean {
return oldItem.resourceId == newItem.resourceId && oldItem.type == newItem.type
}
override fun areContentsTheSame(oldItem: SearchResultDto, newItem: SearchResultDto): Boolean {
return oldItem == newItem
}
}
}
inner class SearchResultViewHolder(val searchResultItemBinding: SearchResultItemBinding) : ViewHolder(searchResultItemBinding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchResultViewHolder {
return SearchResultViewHolder(SearchResultItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: SearchResultViewHolder, position: Int) {
val searchResult = getItem(position)
holder.searchResultItemBinding.apply {
searchResultTitle.text = searchResult.title
searchResultDescription.text = searchResult.description
root.setOnClickListener { onItemClick(searchResult) }
// TODO complete this
val image = when(searchResult.type) {
SearchResultType.Profile -> {
val userId = searchResult.resourceId.toIntOrNull()
if(userId != null) {
UrlGen.userProfileImage(userId)
} else {
R.drawable.baseline_search_24
}
}
SearchResultType.Hashtag -> R.drawable.hashtag_solid
SearchResultType.Post -> R.drawable.baseline_search_24
else -> R.drawable.baseline_search_24
}
searchResultImage.load(image)
}
}
}

View File

@ -10,18 +10,22 @@ import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.carousel.CarouselLayoutManager
import com.google.android.material.carousel.UncontainedCarouselStrategy
import com.google.android.material.chip.Chip
import com.isolaatti.R
import com.isolaatti.databinding.FragmentSearchBinding
import com.isolaatti.hashtags.ui.HashtagsActivity
import com.isolaatti.posting.posts.viewer.ui.PostViewerActivity
import com.isolaatti.profile.ui.ProfileActivity
import com.isolaatti.search.data.HashtagsDto
import com.isolaatti.search.data.NewestUsersDto
import com.isolaatti.search.data.SearchDto
import com.isolaatti.search.data.SearchHistoryEntity
import com.isolaatti.search.data.SearchResultType
import com.isolaatti.search.presentation.SearchResultsAdapter
import com.isolaatti.search.presentation.SearchSuggestionsAdapter
import com.isolaatti.search.presentation.SearchViewModel
import com.isolaatti.search.presentation.UserCarouselAdapter
@ -38,6 +42,7 @@ class SearchFragment : Fragment() {
private val viewModel: SearchViewModel by viewModels()
private var searchSuggestionsAdapter: SearchSuggestionsAdapter? = null
private var newestUsersAdapter: UserCarouselAdapter? = null
private var searchResultsAdapter: SearchResultsAdapter? = null
private val searchSuggestionsObserver: Observer<List<SearchHistoryEntity>> = Observer {
searchSuggestionsAdapter?.submitList(it)
@ -45,6 +50,7 @@ class SearchFragment : Fragment() {
private val searchResultsObserver: Observer<SearchDto> = Observer {
Log.d(LOG_TAG, it.toString())
searchResultsAdapter?.submitList(it.result)
}
private val trendingHashtagsObserver: Observer<HashtagsDto> = Observer {
@ -55,7 +61,7 @@ class SearchFragment : Fragment() {
viewBinding.chipGroup.addView(Chip(requireContext()).apply {
text = "#$hashtag"
setOnClickListener {
requireContext().startActivity(Intent(requireContext(), HashtagsActivity::class.java))
findNavController().navigate(SearchFragmentDirections.actionSearchFragmentToHashtagPostsFragment(hashtag))
}
})
}
@ -123,7 +129,24 @@ class SearchFragment : Fragment() {
layoutManager = CarouselLayoutManager(UncontainedCarouselStrategy())
}
searchResultsAdapter = SearchResultsAdapter(
onItemClick = {
when(it.type) {
SearchResultType.Profile -> {
it.resourceId.toIntOrNull()?.also { ProfileActivity.startActivity(requireContext(), it) }
}
SearchResultType.Post -> {
it.resourceId.toLongOrNull()?.also { PostViewerActivity.startActivity(requireContext(), it) }
}
SearchResultType.Hashtag -> {
findNavController().navigate(SearchFragmentDirections.actionSearchFragmentToHashtagPostsFragment(it.resourceId))
}
SearchResultType.Unknown -> {}
}
}
)
viewBinding.recyclerViewSearchResults.adapter = searchResultsAdapter
viewBinding.recyclerViewSearchResults.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
}
private fun setupListeners() {
@ -147,11 +170,19 @@ class SearchFragment : Fragment() {
if(it.itemId == R.id.close_button) {
showResults(false)
true
} else {
false
}
false
}
viewBinding.searchView.addTransitionListener { searchView, transitionState, transitionState2 -> }
viewBinding.openHashtagsButton.setOnClickListener {
findNavController().navigate(SearchFragmentDirections.actionSearchFragmentToHashtagsFragment())
}
viewBinding.browseProfilesButton.setOnClickListener {
findNavController().navigate(SearchFragmentDirections.actionSearchFragmentToBrowseProfilesFragment())
}
}
private fun setupObservers() {

View File

@ -17,7 +17,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.isolaatti.BuildConfig
import com.isolaatti.R
import com.isolaatti.databinding.FragmentMakeAccountBinding
import com.isolaatti.home.HomeActivity
import com.isolaatti.home.ui.HomeActivity
import com.isolaatti.sign_up.domain.entity.SignUpResultCode
import com.isolaatti.sign_up.presentation.MakeAccountViewModel
import com.isolaatti.sign_up.presentation.SignUpViewModel

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM14,17L7,17v-2h7v2zM17,13L7,13v-2h10v2zM17,9L7,9L7,7h10v2z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="448dp"
android:height="512dp"
android:viewportWidth="448"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M181.3,32.4c17.4,2.9 29.2,19.4 26.3,36.8L197.8,128h95.1l11.5,-69.3c2.9,-17.4 19.4,-29.2 36.8,-26.3s29.2,19.4 26.3,36.8L357.8,128H416c17.7,0 32,14.3 32,32s-14.3,32 -32,32H347.1L325.8,320H384c17.7,0 32,14.3 32,32s-14.3,32 -32,32H315.1l-11.5,69.3c-2.9,17.4 -19.4,29.2 -36.8,26.3s-29.2,-19.4 -26.3,-36.8l9.8,-58.7H155.1l-11.5,69.3c-2.9,17.4 -19.4,29.2 -36.8,26.3s-29.2,-19.4 -26.3,-36.8L90.2,384H32c-17.7,0 -32,-14.3 -32,-32s14.3,-32 32,-32h68.9l21.3,-128H64c-17.7,0 -32,-14.3 -32,-32s14.3,-32 32,-32h68.9l11.5,-69.3c2.9,-17.4 19.4,-29.2 36.8,-26.3zM187.1,192L165.8,320h95.1l21.3,-128H187.1z"/>
</vector>

View File

@ -17,11 +17,11 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_drawer"
app:layout_constraintTop_toTopOf="parent"
tools:context=".home.FeedFragment">
tools:context=".home.ui.FeedFragment">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/topAppBar_layout"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -46,26 +46,11 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_to_refresh"
<androidx.fragment.app.FragmentContainerView
android:id="@+id/post_list_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/topAppBar_layout">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/feed_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/loading_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:layout_gravity="bottom"/>
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView

View File

@ -2,7 +2,8 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
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">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
@ -48,6 +49,13 @@
android:text="@string/app_description"
android:layout_marginHorizontal="24dp"
android:textAlignment="center"/>
<TextView
android:id="@+id/app_version"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="v 1.0"
android:layout_marginHorizontal="24dp"
android:textAlignment="center"/>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -13,7 +13,7 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".home.FeedFragment">
tools:context=".home.ui.FeedFragment">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/topAppBar_layout"
@ -36,23 +36,14 @@
</com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_to_refresh"
<androidx.fragment.app.FragmentContainerView
android:id="@+id/post_list_fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/feed_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/loading_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:layout_gravity="bottom"/>
app:defaultNavHost="true"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:navigationIcon="@drawable/baseline_close_24"
app:title="@string/hashtags"/>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_to_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/feed_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/loading_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:navigationIcon="@drawable/baseline_arrow_back_24"
app:titleCentered="true"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/post_list_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -52,11 +52,12 @@
</com.google.android.material.chip.ChipGroup>
<com.google.android.material.button.MaterialButton
android:id="@+id/open_hashtags_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toBottomOf="@id/chip_group"
android:layout_marginTop="8dp"
android:text="@string/see_all"/>
android:text="@string/go_to_hashtags"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
@ -90,11 +91,12 @@
app:layout_constraintTop_toBottomOf="@id/new_users_card_title"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/browse_profiles_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toBottomOf="@id/new_users_recycler_view"
android:layout_marginTop="8dp"
android:text="@string/see_all"/>
android:text="@string/browse_profiles"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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="70dp"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="4dp"
style="?attr/materialCardViewFilledStyle">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/search_result_image"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/baseline_search_24"/>
<TextView
android:id="@+id/search_result_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/search_result_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/search_result_image"
app:layout_constraintTop_toTopOf="parent"
android:maxLines="1"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
tools:text="Title" />
<TextView
android:id="@+id/search_result_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/search_result_image"
app:layout_constraintTop_toBottomOf="@+id/search_result_title"
android:maxLines="1"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
tools:text="Description" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -5,12 +5,12 @@
android:id="@+id/feedFragment"
android:icon="@drawable/baseline_home_24"
android:title="@string/home" />
<item
android:id="@+id/notificationsFragment"
android:icon="@drawable/baseline_notifications_24"
android:title="@string/notifications" />
<item
android:id="@+id/searchFragment"
android:icon="@drawable/baseline_search_24"
android:title="@string/search" />
<item
android:id="@+id/notificationsFragment"
android:icon="@drawable/baseline_notifications_24"
android:title="@string/notifications" />
</menu>

View File

@ -7,7 +7,7 @@
<fragment
android:id="@+id/feedFragment"
android:name="com.isolaatti.home.FeedFragment"
android:name="com.isolaatti.home.ui.FeedFragment"
android:label="fragment_feed"
tools:layout="@layout/fragment_feed" >
<action
@ -30,5 +30,35 @@
<fragment
android:id="@+id/searchFragment"
android:name="com.isolaatti.search.ui.SearchFragment"
android:label="@string/search" />
android:label="@string/search" >
<action
android:id="@+id/action_searchFragment_to_hashtagsFragment"
app:destination="@id/hashtagsFragment" />
<action
android:id="@+id/action_searchFragment_to_hashtagPostsFragment"
app:destination="@id/hashtagPostsFragment2" />
<action
android:id="@+id/action_searchFragment_to_browseProfilesFragment"
app:destination="@id/browseProfilesFragment" />
</fragment>
<fragment
android:id="@+id/hashtagsFragment"
android:name="com.isolaatti.hashtags.ui.HashtagsFragment"
android:label="HashtagsFragment" >
<action
android:id="@+id/action_hashtagsFragment2_to_hashtagPostsFragment2"
app:destination="@id/hashtagPostsFragment2" />
</fragment>
<fragment
android:id="@+id/hashtagPostsFragment2"
android:name="com.isolaatti.hashtags.ui.HashtagPostsFragment"
android:label="HashtagPostsFragment">
<argument
android:name="hashtag"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/browseProfilesFragment"
android:name="com.isolaatti.profile.ui.BrowseProfilesFragment"
android:label="BrowseProfilesFragment" />
</navigation>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/post_listing_navigation"
app:startDestination="@id/postListingFragment">
<fragment
android:id="@+id/postListingFragment"
android:name="com.isolaatti.posting.posts.ui.PostListingFragment"
android:label="PostListingFragment" >
<argument
android:name="hashtag"
app:argType="string"
app:nullable="true" />
</fragment>
</navigation>

View File

@ -201,5 +201,8 @@
<string name="hashtags">Hashtags</string>
<string name="newest_profiles">Newest profiles</string>
<string name="see_all">See all</string>
<string name="go_to_hashtags">Go to hashtags</string>
<string name="browse_profiles">Browse profiles</string>
<string name="app_version">v%s</string>
<string name="new_report">New report</string>
</resources>

View File

@ -13,8 +13,8 @@ buildscript {
}
}
plugins {
id 'com.android.application' version '8.3.0' apply false
id 'com.android.library' version '8.3.0' apply false
id 'com.android.application' version '8.3.1' apply false
id 'com.android.library' version '8.3.1' apply false
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
id 'com.google.dagger.hilt.android' version '2.47' apply false
id 'com.google.gms.google-services' version '4.4.0' apply false