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> </LayoutPositions>
</value> </value>
</entry> </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"> <entry key="home_navigation.xml">
<value> <value>
<LayoutPositions> <LayoutPositions>
<option name="myPositions"> <option name="myPositions">
<map> <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"> <entry key="feedFragment">
<value> <value>
<LayoutPositions> <LayoutPositions>
@ -85,6 +140,39 @@
</LayoutPositions> </LayoutPositions>
</value> </value>
</entry> </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"> <entry key="notificationsFragment">
<value> <value>
<LayoutPositions> <LayoutPositions>
@ -106,18 +194,6 @@
</LayoutPositions> </LayoutPositions>
</value> </value>
</entry> </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"> <entry key="profileActivity">
<value> <value>
<LayoutPositions> <LayoutPositions>
@ -135,10 +211,29 @@
<LayoutPositions> <LayoutPositions>
<option name="myPosition"> <option name="myPosition">
<Point> <Point>
<option name="x" value="12" /> <option name="x" value="-278" />
<option name="y" value="12" /> <option name="y" value="216" />
</Point> </Point>
</option> </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> </LayoutPositions>
</value> </value>
</entry> </entry>
@ -246,6 +341,28 @@
</LayoutPositions> </LayoutPositions>
</value> </value>
</entry> </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"> <entry key="profile_navigation.xml">
<value> <value>
<LayoutPositions> <LayoutPositions>

View File

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

View File

@ -26,15 +26,15 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </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=".login.LogInActivity" android:theme="@style/Theme.Isolaatti" />
<activity android:name=".profile.ui.ProfileActivity" <activity android:name=".profile.ui.ProfileActivity"
android:theme="@style/Theme.Isolaatti" 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=".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.ui.CreatePostActivity" android:theme="@style/Theme.Isolaatti" android:windowSoftInputMode="adjustResize"/>
<activity android:name=".posting.posts.viewer.ui.PostViewerActivity" android:theme="@style/Theme.Isolaatti" <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=".drafts.ui.DraftsActivity" android:theme="@style/Theme.Isolaatti"/>
<activity android:name=".about.AboutActivity" 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"/> <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.activity.result.contract.ActivityResultContracts
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.isolaatti.auth.data.AuthRepositoryImpl import com.isolaatti.auth.data.AuthRepositoryImpl
import com.isolaatti.home.HomeActivity import com.isolaatti.home.ui.HomeActivity
import com.isolaatti.login.LogInActivity import com.isolaatti.login.LogInActivity
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import javax.inject.Inject

View File

@ -5,6 +5,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.net.toUri import androidx.core.net.toUri
import com.isolaatti.BuildConfig import com.isolaatti.BuildConfig
import com.isolaatti.R
import com.isolaatti.databinding.ActivityAboutBinding import com.isolaatti.databinding.ActivityAboutBinding
class AboutActivity : AppCompatActivity() { class AboutActivity : AppCompatActivity() {
@ -51,5 +52,7 @@ class AboutActivity : AppCompatActivity() {
.build() .build()
.launchUrl(this, BuildConfig.terms.toUri()) .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.os.Bundle
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity 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.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.connectivity.ConnectivityCallbackImpl
import com.isolaatti.connectivity.NetworkStatus import com.isolaatti.connectivity.NetworkStatus
import com.isolaatti.home.HomeActivity
import com.isolaatti.login.LogInActivity import com.isolaatti.login.LogInActivity
import com.isolaatti.utils.Resource import com.isolaatti.utils.Resource
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint

View File

@ -26,6 +26,12 @@ class RetrofitClient @Inject constructor(private val authenticationInterceptor:
private val okHttpClient get() = OkHttpClient.Builder() private val okHttpClient get() = OkHttpClient.Builder()
.addInterceptor(authenticationInterceptor) .addInterceptor(authenticationInterceptor)
.addInterceptor { chain ->
chain.proceed(chain.request()
.newBuilder()
.header("User-Agent", "Isolaatti Android ${BuildConfig.VERSION_NAME}")
.build())
}
.connectTimeout(5, TimeUnit.MINUTES) .connectTimeout(5, TimeUnit.MINUTES)
.writeTimeout(5, TimeUnit.MINUTES) .writeTimeout(5, TimeUnit.MINUTES)
.readTimeout(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.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.isolaatti.auth.domain.AuthRepository import com.isolaatti.auth.domain.AuthRepository
import com.isolaatti.posting.posts.domain.PostsRepository import com.isolaatti.posting.posts.domain.PostsRepository
import com.isolaatti.posting.posts.presentation.PostListingViewModelBase import com.isolaatti.posting.posts.presentation.PostListingViewModelBase
import com.isolaatti.posting.posts.presentation.UpdateEvent 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.entity.UserProfile
import com.isolaatti.profile.domain.use_case.GetProfile import com.isolaatti.profile.domain.use_case.GetProfile
import com.isolaatti.utils.Resource import com.isolaatti.utils.Resource
@ -22,9 +22,8 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class FeedViewModel @Inject constructor( class FeedViewModel @Inject constructor(
private val getProfileUseCase: GetProfile, private val getProfileUseCase: GetProfile,
private val authRepository: AuthRepository, private val authRepository: AuthRepository
private val postsRepository: PostsRepository ) : ViewModel() {
) : PostListingViewModelBase() {
private val toRetry: MutableList<Runnable> = mutableListOf() private val toRetry: MutableList<Runnable> = mutableListOf()
@ -39,41 +38,7 @@ class FeedViewModel @Inject constructor(
toRetry.clear() 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 // User profile
private val _userProfile: MutableLiveData<UserProfile> = MutableLiveData() private val _userProfile: MutableLiveData<UserProfile> = MutableLiveData()
@ -88,7 +53,6 @@ class FeedViewModel @Inject constructor(
when(profile) { when(profile) {
is Resource.Error -> { is Resource.Error -> {
errorLoading.postValue(profile.errorType)
toRetry.add { toRetry.add {
getProfile() 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.Manifest
import android.content.SharedPreferences import android.content.SharedPreferences
@ -45,10 +45,6 @@ class HomeActivity : IsolaattiBaseActivity() {
askNotificationPermission() askNotificationPermission()
if(savedInstanceState == null) {
feedViewModel.getFeed(false)
}
} }
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {

View File

@ -1,5 +1,6 @@
package com.isolaatti.posting.posts.data.remote package com.isolaatti.posting.posts.data.remote
import com.isolaatti.common.ResultDto
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
@ -22,4 +23,7 @@ interface FeedsApi {
@GET("Feed") @GET("Feed")
fun getChronology(@Query("lastId") lastId: Long, @Query("length") length: Int): Call<FeedDto> 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 package com.isolaatti.posting.posts.data.repository
import android.util.Log
import com.google.gson.Gson import com.google.gson.Gson
import com.isolaatti.posting.posts.data.remote.CreatePostDto import com.isolaatti.posting.posts.data.remote.CreatePostDto
import com.isolaatti.posting.posts.data.remote.DeletePostDto import com.isolaatti.posting.posts.data.remote.DeletePostDto
@ -18,10 +19,18 @@ import retrofit2.awaitResponse
import javax.inject.Inject import javax.inject.Inject
class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, private val postApi: PostApi) : PostsRepository { 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()) emit(Resource.Loading())
try { 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) { if(result.isSuccessful) {
emit(Resource.Success(result.body()?.let { Post.fromFeedDto(it) })) emit(Resource.Success(result.body()?.let { Post.fromFeedDto(it) }))
return@flow return@flow
@ -32,7 +41,8 @@ class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi, pr
500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError)) 500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError))
else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError)) 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)) emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
} }
} }

View File

@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.Flow
interface PostsRepository { 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>>> 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 } 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) { fun likePost(postId: Long) {
viewModelScope.launch { viewModelScope.launch {

View File

@ -361,7 +361,9 @@ class PostsRecyclerViewAdapter (
if(totalItems != null && totalItems > 0 && !requestedNewContent) { if(totalItems != null && totalItems > 0 && !requestedNewContent) {
if(position == totalItems - 1) { if(position == totalItems - 1) {
requestedNewContent = true 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 android.os.Bundle
import androidx.fragment.app.Fragment import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import androidx.fragment.app.Fragment
import android.widget.TextView
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager 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.BuildConfig
import com.isolaatti.R
import com.isolaatti.about.AboutActivity
import com.isolaatti.audio.common.domain.Audio import com.isolaatti.audio.common.domain.Audio
import com.isolaatti.audio.common.domain.Playable import com.isolaatti.audio.common.domain.Playable
import com.isolaatti.audio.player.AudioPlayerConnector 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.Dialogs
import com.isolaatti.common.ErrorMessageViewModel 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.OnUserInteractedWithPostCallback
import com.isolaatti.common.Ownable import com.isolaatti.common.Ownable
import com.isolaatti.common.options_bottom_sheet.domain.OptionClicked 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.domain.Options
import com.isolaatti.common.options_bottom_sheet.presentation.BottomSheetPostOptionsViewModel import com.isolaatti.common.options_bottom_sheet.presentation.BottomSheetPostOptionsViewModel
import com.isolaatti.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment 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.domain.entity.Post
import com.isolaatti.posting.posts.presentation.CreatePostContract
import com.isolaatti.posting.posts.presentation.EditPostContract 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.presentation.PostsRecyclerViewAdapter
import com.isolaatti.posting.posts.viewer.ui.PostViewerActivity
import com.isolaatti.profile.ui.ProfileActivity 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 dagger.hilt.android.AndroidEntryPoint
import io.noties.markwon.AbstractMarkwonPlugin import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon 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.coil.CoilImagesPlugin
import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute
import io.noties.markwon.linkify.LinkifyPlugin import io.noties.markwon.linkify.LinkifyPlugin
import kotlinx.coroutines.launch
import org.w3c.dom.Text
@AndroidEntryPoint @AndroidEntryPoint
class FeedFragment : Fragment(), OnUserInteractedWithPostCallback { class PostListingFragment : Fragment(), OnUserInteractedWithPostCallback {
companion object { companion object {
fun newInstance() = FeedFragment() const val ARG_HASHTAG = "hashtag"
const val CALLER_ID = 20 const val LOG_TAG = "PostListingFragment"
} }
var hashtag: String? = null
private lateinit var viewBinding: FragmentPostListingBinding
private val errorViewModel: ErrorMessageViewModel by activityViewModels() private val errorViewModel: ErrorMessageViewModel by activityViewModels()
private val viewModel: FeedViewModel by activityViewModels() private val viewModel: PostListingViewModel by viewModels()
val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels() val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels()
private var currentUserId = 0
private lateinit var viewBinding: FragmentFeedBinding
private lateinit var adapter: PostsRecyclerViewAdapter private lateinit var adapter: PostsRecyclerViewAdapter
private lateinit var audioPlayerConnector: AudioPlayerConnector 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 { private val audioPlayerConnectorListener = object: AudioPlayerConnector.Listener {
override fun onPlaying(isPlaying: Boolean, audio: Playable) { override fun onPlaying(isPlaying: Boolean, audio: Playable) {
if(audio is Audio) 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) 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 = AudioPlayerConnector(requireContext())
audioPlayerConnector.addListener(audioPlayerConnectorListener) audioPlayerConnector.addListener(audioPlayerConnectorListener)
@ -192,11 +112,12 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
.usePlugin(object: AbstractMarkwonPlugin() { .usePlugin(object: AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder builder
.imageDestinationProcessor(ImageDestinationProcessorRelativeToAbsolute .imageDestinationProcessor(
ImageDestinationProcessorRelativeToAbsolute
.create(BuildConfig.backend)) .create(BuildConfig.backend))
} }
}) })
.usePlugin(CoilImagesPlugin.create(requireContext(), imageLoader)) .usePlugin(CoilImagesPlugin.create(requireContext(), CoilImageLoader.imageLoader))
.usePlugin(LinkifyPlugin.create()) .usePlugin(LinkifyPlugin.create())
.build() .build()
adapter = PostsRecyclerViewAdapter(markwon, this) adapter = PostsRecyclerViewAdapter(markwon, this)
@ -206,44 +127,9 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
viewBinding.swipeToRefresh.setOnRefreshListener { 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){ viewModel.posts.observe(viewLifecycleOwner){
if (it?.first != null) { if (it?.first != null) {
adapter.updateList(it.first!!, it.second) adapter.updateList(it.first!!, it.second)
@ -263,18 +149,7 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
errorViewModel.error.postValue(it) errorViewModel.error.postValue(it)
} }
viewModel.getProfile()
optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver) 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) 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 onUnLiked(postId: Long) = viewModel.unLikePost(postId)
override fun onOptions(post: Ownable) { 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() val modalBottomSheet = BottomSheetPostOptionsFragment()
modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostOptionsFragment.TAG) modalBottomSheet.show(requireActivity().supportFragmentManager, BottomSheetPostOptionsFragment.TAG)
} }
@ -305,8 +180,39 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
} }
override fun onLoadMore() { 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 { viewModelScope.launch {
if(refresh) { if(refresh) {
posts.value = Pair(null, UpdateEvent(UpdateEvent.UpdateType.REFRESH, null)) posts.value = Pair(null, UpdateEvent(UpdateEvent.UpdateType.REFRESH, null))
@ -132,7 +132,7 @@ class ProfileViewModel @Inject constructor(
is Resource.Error -> { is Resource.Error -> {
errorLoading.postValue(feedDtoResource.errorType) errorLoading.postValue(feedDtoResource.errorType)
toRetry.add { 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.feedRecyclerView.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
viewBinding.swipeToRefresh.setOnRefreshListener { viewBinding.swipeToRefresh.setOnRefreshListener {
viewModel.getFeed(true) viewModel.getFeed(true, null)
} }
viewBinding.playButton.setOnClickListener { viewBinding.playButton.setOnClickListener {
audioDescriptionAudio?.let { audio -> audioDescriptionAudio?.let { audio ->
@ -398,7 +398,7 @@ class ProfileMainFragment : Fragment() {
userId?.let { profileId -> userId?.let { profileId ->
viewModel.profileId = profileId viewModel.profileId = profileId
viewModel.getProfile() viewModel.getProfile()
viewModel.getFeed(true) viewModel.getFeed(true, null)
} }
} }
@ -468,7 +468,7 @@ class ProfileMainFragment : Fragment() {
} }
override fun onLoadMore() { override fun onLoadMore() {
viewModel.getFeed(false) viewModel.getFeed(false, null)
} }
} }
} }

View File

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

View File

@ -1,14 +1,33 @@
package com.isolaatti.search.data 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 ProfileSearchDto(val id: Int, val name: String, val imageId: String?, val following: Boolean)
data class SearchDto( data class SearchDto(val result: List<SearchResultDto>)
val profiles: List<ProfileSearchDto>,
val posts: List<FeedDto.PostDto> data class SearchResultDto(
// TODO add the other types 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>) 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.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.carousel.CarouselLayoutManager import com.google.android.material.carousel.CarouselLayoutManager
import com.google.android.material.carousel.UncontainedCarouselStrategy import com.google.android.material.carousel.UncontainedCarouselStrategy
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.databinding.FragmentSearchBinding 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.profile.ui.ProfileActivity
import com.isolaatti.search.data.HashtagsDto import com.isolaatti.search.data.HashtagsDto
import com.isolaatti.search.data.NewestUsersDto import com.isolaatti.search.data.NewestUsersDto
import com.isolaatti.search.data.SearchDto import com.isolaatti.search.data.SearchDto
import com.isolaatti.search.data.SearchHistoryEntity 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.SearchSuggestionsAdapter
import com.isolaatti.search.presentation.SearchViewModel import com.isolaatti.search.presentation.SearchViewModel
import com.isolaatti.search.presentation.UserCarouselAdapter import com.isolaatti.search.presentation.UserCarouselAdapter
@ -38,6 +42,7 @@ class SearchFragment : Fragment() {
private val viewModel: SearchViewModel by viewModels() private val viewModel: SearchViewModel by viewModels()
private var searchSuggestionsAdapter: SearchSuggestionsAdapter? = null private var searchSuggestionsAdapter: SearchSuggestionsAdapter? = null
private var newestUsersAdapter: UserCarouselAdapter? = null private var newestUsersAdapter: UserCarouselAdapter? = null
private var searchResultsAdapter: SearchResultsAdapter? = null
private val searchSuggestionsObserver: Observer<List<SearchHistoryEntity>> = Observer { private val searchSuggestionsObserver: Observer<List<SearchHistoryEntity>> = Observer {
searchSuggestionsAdapter?.submitList(it) searchSuggestionsAdapter?.submitList(it)
@ -45,6 +50,7 @@ class SearchFragment : Fragment() {
private val searchResultsObserver: Observer<SearchDto> = Observer { private val searchResultsObserver: Observer<SearchDto> = Observer {
Log.d(LOG_TAG, it.toString()) Log.d(LOG_TAG, it.toString())
searchResultsAdapter?.submitList(it.result)
} }
private val trendingHashtagsObserver: Observer<HashtagsDto> = Observer { private val trendingHashtagsObserver: Observer<HashtagsDto> = Observer {
@ -55,7 +61,7 @@ class SearchFragment : Fragment() {
viewBinding.chipGroup.addView(Chip(requireContext()).apply { viewBinding.chipGroup.addView(Chip(requireContext()).apply {
text = "#$hashtag" text = "#$hashtag"
setOnClickListener { setOnClickListener {
requireContext().startActivity(Intent(requireContext(), HashtagsActivity::class.java)) findNavController().navigate(SearchFragmentDirections.actionSearchFragmentToHashtagPostsFragment(hashtag))
} }
}) })
} }
@ -123,7 +129,24 @@ class SearchFragment : Fragment() {
layoutManager = CarouselLayoutManager(UncontainedCarouselStrategy()) 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() { private fun setupListeners() {
@ -147,11 +170,19 @@ class SearchFragment : Fragment() {
if(it.itemId == R.id.close_button) { if(it.itemId == R.id.close_button) {
showResults(false) showResults(false)
true true
} else {
false
} }
false
}
viewBinding.openHashtagsButton.setOnClickListener {
findNavController().navigate(SearchFragmentDirections.actionSearchFragmentToHashtagsFragment())
}
viewBinding.browseProfilesButton.setOnClickListener {
findNavController().navigate(SearchFragmentDirections.actionSearchFragmentToBrowseProfilesFragment())
} }
viewBinding.searchView.addTransitionListener { searchView, transitionState, transitionState2 -> }
} }
private fun setupObservers() { private fun setupObservers() {

View File

@ -17,7 +17,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.isolaatti.BuildConfig import com.isolaatti.BuildConfig
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.databinding.FragmentMakeAccountBinding 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.domain.entity.SignUpResultCode
import com.isolaatti.sign_up.presentation.MakeAccountViewModel import com.isolaatti.sign_up.presentation.MakeAccountViewModel
import com.isolaatti.sign_up.presentation.SignUpViewModel 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_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_drawer" app:layout_constraintStart_toEndOf="@+id/home_drawer"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:context=".home.FeedFragment"> tools:context=".home.ui.FeedFragment">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/topAppBar_layout" android:id="@+id/topAppBar_layout"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -46,26 +46,11 @@
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.fragment.app.FragmentContainerView
android:id="@+id/swipe_to_refresh" android:id="@+id/post_list_fragment_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" 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"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView <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" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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 <com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout" android:id="@+id/appBarLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -48,6 +49,13 @@
android:text="@string/app_description" android:text="@string/app_description"
android:layout_marginHorizontal="24dp" android:layout_marginHorizontal="24dp"
android:textAlignment="center"/> 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 <com.google.android.material.card.MaterialCardView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -13,7 +13,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.FeedFragment"> tools:context=".home.ui.FeedFragment">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/topAppBar_layout" android:id="@+id/topAppBar_layout"
@ -36,23 +36,14 @@
</com.google.android.material.appbar.AppBarLayout> </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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> app:defaultNavHost="true"
<androidx.recyclerview.widget.RecyclerView app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
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"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </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.chip.ChipGroup>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/open_hashtags_button"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_constraintTop_toBottomOf="@id/chip_group" app:layout_constraintTop_toBottomOf="@id/chip_group"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:text="@string/see_all"/> android:text="@string/go_to_hashtags"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<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"/> app:layout_constraintTop_toBottomOf="@id/new_users_card_title"/>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/browse_profiles_button"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_constraintTop_toBottomOf="@id/new_users_recycler_view" app:layout_constraintTop_toBottomOf="@id/new_users_recycler_view"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:text="@string/see_all"/> android:text="@string/browse_profiles"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
</LinearLayout> </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:id="@+id/feedFragment"
android:icon="@drawable/baseline_home_24" android:icon="@drawable/baseline_home_24"
android:title="@string/home" /> android:title="@string/home" />
<item
android:id="@+id/notificationsFragment"
android:icon="@drawable/baseline_notifications_24"
android:title="@string/notifications" />
<item <item
android:id="@+id/searchFragment" android:id="@+id/searchFragment"
android:icon="@drawable/baseline_search_24" android:icon="@drawable/baseline_search_24"
android:title="@string/search" /> android:title="@string/search" />
<item
android:id="@+id/notificationsFragment"
android:icon="@drawable/baseline_notifications_24"
android:title="@string/notifications" />
</menu> </menu>

View File

@ -7,7 +7,7 @@
<fragment <fragment
android:id="@+id/feedFragment" android:id="@+id/feedFragment"
android:name="com.isolaatti.home.FeedFragment" android:name="com.isolaatti.home.ui.FeedFragment"
android:label="fragment_feed" android:label="fragment_feed"
tools:layout="@layout/fragment_feed" > tools:layout="@layout/fragment_feed" >
<action <action
@ -30,5 +30,35 @@
<fragment <fragment
android:id="@+id/searchFragment" android:id="@+id/searchFragment"
android:name="com.isolaatti.search.ui.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> </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="hashtags">Hashtags</string>
<string name="newest_profiles">Newest profiles</string> <string name="newest_profiles">Newest profiles</string>
<string name="see_all">See all</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> <string name="new_report">New report</string>
</resources> </resources>

View File

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