WIP 2 search

This commit is contained in:
erik-everardo 2024-03-29 00:10:55 -06:00
parent d815d717e2
commit 8730bbe796
22 changed files with 730 additions and 19 deletions

View File

@ -44,6 +44,7 @@
<activity android:name=".profile.ui.EditProfileActivity" android:theme="@style/Theme.Isolaatti" /> <activity android:name=".profile.ui.EditProfileActivity" android:theme="@style/Theme.Isolaatti" />
<activity android:name=".audio.recorder.ui.AudioRecorderActivity" android:theme="@style/Theme.Isolaatti" /> <activity android:name=".audio.recorder.ui.AudioRecorderActivity" android:theme="@style/Theme.Isolaatti" />
<activity android:name=".audio.audio_selector.ui.AudioSelectorActivity" android:theme="@style/Theme.Isolaatti" /> <activity android:name=".audio.audio_selector.ui.AudioSelectorActivity" android:theme="@style/Theme.Isolaatti" />
<activity android:name=".hashtags.ui.HashtagsActivity" android:theme="@style/Theme.Isolaatti" />
<provider <provider
android:authorities="com.isolaatti.provider" android:authorities="com.isolaatti.provider"
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"

View File

@ -11,7 +11,7 @@ import com.isolaatti.search.data.SearchHistoryEntity
import com.isolaatti.settings.data.KeyValueDao import com.isolaatti.settings.data.KeyValueDao
import com.isolaatti.settings.data.KeyValueEntity import com.isolaatti.settings.data.KeyValueEntity
@Database(entities = [KeyValueEntity::class, UserInfoEntity::class, AudioDraftEntity::class, SearchHistoryEntity::class], version = 6) @Database(entities = [KeyValueEntity::class, UserInfoEntity::class, AudioDraftEntity::class, SearchHistoryEntity::class], version = 7)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun keyValueDao(): KeyValueDao abstract fun keyValueDao(): KeyValueDao
abstract fun userInfoDao(): UserInfoDao abstract fun userInfoDao(): UserInfoDao

View File

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

@ -1,7 +1,11 @@
package com.isolaatti.search package com.isolaatti.search
import com.isolaatti.connectivity.RetrofitClient import com.isolaatti.connectivity.RetrofitClient
import com.isolaatti.database.AppDatabase
import com.isolaatti.search.data.SearchApi import com.isolaatti.search.data.SearchApi
import com.isolaatti.search.data.SearchDao
import com.isolaatti.search.data.SearchRepositoryImpl
import com.isolaatti.search.domain.SearchRepository
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -14,4 +18,14 @@ class Module {
fun provideSearchApi(retrofitClient: RetrofitClient): SearchApi { fun provideSearchApi(retrofitClient: RetrofitClient): SearchApi {
return retrofitClient.client.create(SearchApi::class.java) return retrofitClient.client.create(SearchApi::class.java)
} }
@Provides
fun provideSearchDao(database: AppDatabase): SearchDao {
return database.searchHistoryDao()
}
@Provides
fun provideSearchRepository(searchApi: SearchApi, searchDao: SearchDao): SearchRepository {
return SearchRepositoryImpl(searchApi, searchDao)
}
} }

View File

@ -1,9 +1,16 @@
package com.isolaatti.search.data package com.isolaatti.search.data
import retrofit2.Call
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Query import retrofit2.http.Query
interface SearchApi { interface SearchApi {
@GET("Search/Quick") @GET("Search/Quick")
fun quickSearch(@Query("q") query: String) fun quickSearch(@Query("q") query: String): Call<SearchDto>
@GET("hashtags/trending")
fun getTrendingHashtags(): Call<HashtagsDto>
@GET("Search/newestUsers")
fun getNewestUsers(@Query("limit") limit: Int, @Query("after") after: Int?): Call<NewestUsersDto>
} }

View File

@ -7,15 +7,32 @@ import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction
@Dao @Dao
interface SearchDao { interface SearchDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertTerm(searchTerm: SearchHistoryEntity) fun insertTerm(searchTerm: SearchHistoryEntity)
@Delete @Query("SELECT COUNT(*) FROM search_history WHERE `query` = :query")
fun deleteTerm(searchTerm: SearchHistoryEntity) fun itemCount(query: String): Int
@Query("SELECT * FROM search_history ORDER BY time DESC") @Transaction
fun getTerms(): LiveData<List<SearchHistoryEntity>> fun insertTermTransaction(searchTerm: SearchHistoryEntity) {
if(itemCount(searchTerm.query) > 0) {
deleteTerm(searchTerm.query)
}
insertTerm(searchTerm)
}
@Query("DELETE FROM search_history WHERE `query` = :searchTerm")
fun deleteTerm(searchTerm: String)
@Query("SELECT *, `rowid` FROM search_history ORDER BY time DESC")
fun getTerms(): List<SearchHistoryEntity>
@Query("""
SELECT *, `rowid` FROM search_history WHERE `query` MATCH :searchQuery
""")
fun getTermsForQuery(searchQuery: String): List<SearchHistoryEntity>
} }

View File

@ -0,0 +1,15 @@
package com.isolaatti.search.data
import com.isolaatti.posting.posts.data.remote.FeedDto
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 HashtagsDto(val result: List<String>)
data class NewestUsersDto(val result: List<ProfileSearchDto>)

View File

@ -1,10 +1,17 @@
package com.isolaatti.search.data package com.isolaatti.search.data
import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Fts4
import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
@Entity(tableName = "search_history") @Entity(tableName = "search_history")
@Fts4
data class SearchHistoryEntity( data class SearchHistoryEntity(
@PrimaryKey val query: String, @PrimaryKey
@ColumnInfo(name = "rowid")
val rowId: Int,
val query: String,
val time: Long val time: Long
) )

View File

@ -0,0 +1,88 @@
package com.isolaatti.search.data
import android.util.Log
import com.isolaatti.search.domain.SearchRepository
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import retrofit2.awaitResponse
class SearchRepositoryImpl(private val searchApi: SearchApi, private val searchDao: SearchDao) : SearchRepository {
companion object {
const val LOG_TAG = "SearchRepositoryImpl"
}
override fun search(query: String): Flow<Resource<SearchDto>> = flow {
searchDao.insertTermTransaction(SearchHistoryEntity(0, query, System.currentTimeMillis()))
try {
val response = searchApi.quickSearch(query).awaitResponse()
if(response.isSuccessful) {
val dto = response.body()
if(dto == null) {
Log.e(LOG_TAG, "Emitting error, could not get body")
emit(Resource.Error(Resource.Error.ErrorType.OtherError))
return@flow
}
emit(Resource.Success(dto))
} else {
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
}
} catch(e: Exception) {
Log.e(LOG_TAG, "Error searching: ${e.message}")
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
}
override fun getSuggestions(query: String): Flow<Resource<List<SearchHistoryEntity>>> = flow {
emit(Resource.Loading())
if(query.isEmpty()) {
emit(Resource.Success(searchDao.getTerms()))
} else {
emit(Resource.Success(searchDao.getTermsForQuery(query)))
}
}
override fun removeSuggestion(query: String): Flow<Resource<Boolean>> = flow {
searchDao.deleteTerm(query)
emit(Resource.Success(true))
}
override fun getTrendingHashtags(): Flow<Resource<HashtagsDto>> = flow {
emit(Resource.Loading())
try {
val result = searchApi.getTrendingHashtags().awaitResponse()
if(result.isSuccessful) {
val dto = result.body()
dto?.also { emit(Resource.Success(it)) }
} else {
emit(Resource.Error(Resource.Error.mapErrorCode(result.code())))
}
} catch(e: Exception) {
Log.e(LOG_TAG, "Error getting trending hashtags: ${e.message}")
}
}
override fun getNewestUsers(): Flow<Resource<NewestUsersDto>> = flow {
emit(Resource.Loading())
try {
val result = searchApi.getNewestUsers(10, null).awaitResponse()
if(result.isSuccessful) {
val dto = result.body()
if(dto != null) {
emit(Resource.Success(dto))
}
} else {
emit(Resource.Error(Resource.Error.mapErrorCode(result.code())))
}
} catch(e: Exception) {
Log.e(LOG_TAG, "Error getting newest users: ${e.message}")
}
}
}

View File

@ -1,8 +1,19 @@
package com.isolaatti.search.domain package com.isolaatti.search.domain
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.utils.Resource import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface SearchRepository { interface SearchRepository {
fun search(query: String): Flow<Resource<SearchDto>>
fun getSuggestions(query: String): Flow<Resource<List<SearchHistoryEntity>>>
fun removeSuggestion(query: String): Flow<Resource<Boolean>>
fun getTrendingHashtags(): Flow<Resource<HashtagsDto>>
fun getNewestUsers(): Flow<Resource<NewestUsersDto>>
} }

View File

@ -0,0 +1,46 @@
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 com.isolaatti.databinding.SearchSuggestionItemBinding
import com.isolaatti.search.data.SearchHistoryEntity
class SearchSuggestionsAdapter(
private val onItemClick: (item: SearchHistoryEntity) -> Unit = {},
private val onItemDeleteClick: (item: SearchHistoryEntity) -> Unit = {}
) : ListAdapter<SearchHistoryEntity, SearchSuggestionsAdapter.SearchSuggestionItemViewHolder>(itemCallback) {
inner class SearchSuggestionItemViewHolder(val searchSuggestionItemBinding: SearchSuggestionItemBinding) : ViewHolder(searchSuggestionItemBinding.root)
companion object {
val itemCallback = object: DiffUtil.ItemCallback<SearchHistoryEntity>() {
override fun areItemsTheSame(oldItem: SearchHistoryEntity, newItem: SearchHistoryEntity): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: SearchHistoryEntity, newItem: SearchHistoryEntity): Boolean {
return oldItem.query == newItem.query
}
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): SearchSuggestionItemViewHolder {
return SearchSuggestionItemViewHolder(SearchSuggestionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: SearchSuggestionItemViewHolder, position: Int) {
val searchSuggestion = getItem(position)
holder.searchSuggestionItemBinding.suggestionText.text = searchSuggestion.query
holder.searchSuggestionItemBinding.root.setOnClickListener { onItemClick(searchSuggestion) }
holder.searchSuggestionItemBinding.deleteButton.setOnClickListener { onItemDeleteClick(searchSuggestion) }
}
}

View File

@ -1,20 +1,111 @@
package com.isolaatti.search.presentation package com.isolaatti.search.presentation
import androidx.lifecycle.LiveData import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.isolaatti.search.data.SearchApi import com.isolaatti.search.data.HashtagsDto
import com.isolaatti.search.data.SearchDao import com.isolaatti.search.data.NewestUsersDto
import com.isolaatti.search.data.SearchDto
import com.isolaatti.search.data.SearchHistoryEntity
import com.isolaatti.search.domain.SearchRepository
import com.isolaatti.utils.Resource
import dagger.hilt.android.lifecycle.HiltViewModel 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 kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SearchViewModel @Inject constructor() : ViewModel() { class SearchViewModel @Inject constructor(private val searchRepository: SearchRepository) : ViewModel() {
fun search(query: String) { companion object {
const val LOG_TAG = "SearchViewModel"
}
val searchSuggestions: MutableLiveData<List<SearchHistoryEntity>> = MutableLiveData()
val searchResults: MutableLiveData<SearchDto> = MutableLiveData()
val trendingHashtags: MutableLiveData<HashtagsDto> = MutableLiveData()
val newestUsers: MutableLiveData<NewestUsersDto> = MutableLiveData()
var searchQuery = ""
fun search() {
Log.d(LOG_TAG, searchQuery)
viewModelScope.launch { viewModelScope.launch {
if(searchQuery.isNotEmpty()) {
searchRepository.search(searchQuery).onEach {
when(it) {
is Resource.Error -> {}
is Resource.Loading -> {}
is Resource.Success -> {
searchResults.postValue(it.data!!)
}
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
} }
} }
fun getSuggestions(query: String) {
searchQuery = query
viewModelScope.launch {
searchRepository.getSuggestions(query).onEach {
when(it) {
is Resource.Error -> {}
is Resource.Loading -> {}
is Resource.Success -> {
searchSuggestions.postValue(it.data!!)
}
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
fun deleteSuggestion(suggestion: SearchHistoryEntity) {
viewModelScope.launch {
searchRepository.removeSuggestion(suggestion.query).onEach { resource ->
if(resource is Resource.Success && resource.data == true) {
val searchSuggestionsMutable = searchSuggestions.value?.toMutableList()
searchSuggestionsMutable?.remove(suggestion)
searchSuggestionsMutable?.also { searchSuggestions.postValue(it) }
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
fun loadTendingHashtags() {
viewModelScope.launch {
searchRepository.getTrendingHashtags().onEach { resource ->
when(resource) {
is Resource.Error -> {}
is Resource.Loading -> {}
is Resource.Success -> {
resource.data?.also { trendingHashtags.postValue(it) }
}
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
fun loadNewestUsers() {
viewModelScope.launch {
searchRepository.getNewestUsers().onEach { resource ->
when(resource) {
is Resource.Error -> {}
is Resource.Loading -> {}
is Resource.Success -> {
resource.data?.also { newestUsers.postValue(it) }
}
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
} }

View File

@ -0,0 +1,58 @@
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.UsersCarouselItemBinding
import com.isolaatti.search.data.ProfileSearchDto
import com.isolaatti.utils.UrlGen
class UserCarouselAdapter(
private val onProfileClick: (profileId: Int) -> Unit = {}
) : ListAdapter<ProfileSearchDto, UserCarouselAdapter.UserCarouselItemViewHolder>(itemCallback) {
companion object {
val itemCallback = object: DiffUtil.ItemCallback<ProfileSearchDto>() {
override fun areItemsTheSame(
oldItem: ProfileSearchDto,
newItem: ProfileSearchDto
): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(
oldItem: ProfileSearchDto,
newItem: ProfileSearchDto
): Boolean {
return oldItem == newItem
}
}
}
inner class UserCarouselItemViewHolder(val usersCarouselItemBinding: UsersCarouselItemBinding) : ViewHolder(usersCarouselItemBinding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserCarouselItemViewHolder {
return UserCarouselItemViewHolder(UsersCarouselItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: UserCarouselItemViewHolder, position: Int) {
val user = getItem(position)
holder.usersCarouselItemBinding.userCarouselName.text = user.name
if(user.imageId != null) {
holder.usersCarouselItemBinding.userCarouselImageView.load(UrlGen.imageUrl(user.imageId))
} else {
holder.usersCarouselItemBinding.userCarouselImageView.load(R.drawable.avatar)
}
holder.usersCarouselItemBinding.userCarouselCard.setOnClickListener { onProfileClick(user.id) }
}
}

View File

@ -1,19 +1,76 @@
package com.isolaatti.search.ui package com.isolaatti.search.ui
import android.content.Intent
import android.os.Bundle import android.os.Bundle
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 androidx.core.widget.doAfterTextChanged 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.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.databinding.FragmentSearchBinding
import com.isolaatti.hashtags.ui.HashtagsActivity
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.presentation.SearchSuggestionsAdapter
import com.isolaatti.search.presentation.SearchViewModel import com.isolaatti.search.presentation.SearchViewModel
import com.isolaatti.search.presentation.UserCarouselAdapter
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class SearchFragment : Fragment() { class SearchFragment : Fragment() {
companion object {
const val LOG_TAG = "SearchFragment"
}
lateinit var viewBinding: FragmentSearchBinding lateinit var viewBinding: FragmentSearchBinding
private val viewModel: SearchViewModel by viewModels() private val viewModel: SearchViewModel by viewModels()
private var searchSuggestionsAdapter: SearchSuggestionsAdapter? = null
private var newestUsersAdapter: UserCarouselAdapter? = null
private val searchSuggestionsObserver: Observer<List<SearchHistoryEntity>> = Observer {
searchSuggestionsAdapter?.submitList(it)
}
private val searchResultsObserver: Observer<SearchDto> = Observer {
Log.d(LOG_TAG, it.toString())
}
private val trendingHashtagsObserver: Observer<HashtagsDto> = Observer {
Log.d(LOG_TAG, it.toString())
viewBinding.chipGroup.removeAllViews()
it.result.forEach { hashtag ->
viewBinding.chipGroup.addView(Chip(requireContext()).apply {
text = "#$hashtag"
setOnClickListener {
requireContext().startActivity(Intent(requireContext(), HashtagsActivity::class.java))
}
})
}
}
private val newestUsersObserver: Observer<NewestUsersDto> = Observer {
newestUsersAdapter?.submitList(it.result)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.loadTendingHashtags()
viewModel.loadNewestUsers()
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -24,23 +81,83 @@ class SearchFragment : Fragment() {
return viewBinding.root return viewBinding.root
} }
private fun showResults(show: Boolean) {
if(show) {
viewBinding.viewAnimator.displayedChild = 1
viewBinding.searchBar.menu.findItem(R.id.close_button)?.setVisible(true)
} else {
viewBinding.viewAnimator.displayedChild = 0
viewModel.searchQuery = ""
viewBinding.searchBar.setText("")
viewBinding.searchBar.menu.findItem(R.id.close_button)?.setVisible(false)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupListeners() setupListeners()
setupObservers() setupObservers()
searchSuggestionsAdapter = SearchSuggestionsAdapter(
onItemClick = {
viewBinding.searchBar.setText(it.query)
viewModel.searchQuery = it.query
viewBinding.searchView.hide()
viewModel.search()
showResults(true)
},
onItemDeleteClick = {
viewModel.deleteSuggestion(it)
}
)
viewBinding.searchHistoryRecyclerView.apply {
adapter = searchSuggestionsAdapter
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
}
newestUsersAdapter = UserCarouselAdapter(
onProfileClick = {
ProfileActivity.startActivity(requireContext(), it)
}
)
viewBinding.newUsersRecyclerView.apply {
adapter = newestUsersAdapter
layoutManager = CarouselLayoutManager(UncontainedCarouselStrategy())
}
} }
private fun setupListeners() { private fun setupListeners() {
viewBinding.searchView.editText.doAfterTextChanged { searchText -> viewBinding.searchView.editText.doAfterTextChanged { searchText ->
if(searchText != null) { if(searchText != null) {
viewModel.search(searchText.toString()) viewModel.getSuggestions(searchText.toString())
} }
} }
viewBinding.searchView.editText.setOnEditorActionListener { v, actionId, event ->
Log.d(LOG_TAG, "actionId: $actionId; event: $event")
viewBinding.searchBar.setText(viewBinding.searchView.text)
viewBinding.searchView.hide()
viewModel.search()
showResults(true)
true
}
viewBinding.searchBar.setOnMenuItemClickListener {
if(it.itemId == R.id.close_button) {
showResults(false)
true
}
false
}
viewBinding.searchView.addTransitionListener { searchView, transitionState, transitionState2 -> }
} }
private fun setupObservers() { private fun setupObservers() {
viewModel.searchSuggestions.observe(viewLifecycleOwner, searchSuggestionsObserver)
viewModel.searchResults.observe(viewLifecycleOwner, searchResultsObserver)
viewModel.trendingHashtags.observe(viewLifecycleOwner, trendingHashtagsObserver)
viewModel.newestUsers.observe(viewLifecycleOwner, newestUsersObserver)
} }
} }

View File

@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
<group>
<clip-path
android:pathData="M0,0h512v512h-512z"/>
<path
android:pathData="M0,0H512V512H0V0Z"
android:fillColor="#222D3A"/>
<path
android:pathData="M330.08,110.96C311.3,90.67 285.06,79.5 256.1,79.5C226.98,79.5 200.65,90.6 181.96,110.76C163.05,131.14 153.84,158.84 156.01,188.75C160.29,247.75 205.19,295.75 256.1,295.75C307,295.75 351.83,247.76 356.18,188.76C358.37,159.13 349.1,131.49 330.08,110.96Z"
android:fillColor="#B3BAC0"/>
<path
android:pathData="M53.93,512H458.33C459,503 458.21,483.5 456.33,473.14C448.18,427.94 422.76,389.97 382.79,363.33C347.28,339.67 302.3,326.64 256.13,326.64C209.96,326.64 164.98,339.67 129.47,363.33C89.5,389.98 64.08,427.95 55.93,473.15C54.05,483.51 53.5,504.5 53.93,512Z"
android:fillColor="#B3BAC0"/>
</group>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M75,75L41,41C25.9,25.9 0,36.6 0,57.9L0,168c0,13.3 10.7,24 24,24L134.1,192c21.4,0 32.1,-25.9 17,-41l-30.8,-30.8C155,85.5 203,64 256,64c106,0 192,86 192,192s-86,192 -192,192c-40.8,0 -78.6,-12.7 -109.7,-34.4c-14.5,-10.1 -34.4,-6.6 -44.6,7.9s-6.6,34.4 7.9,44.6C151.2,495 201.7,512 256,512c141.4,0 256,-114.6 256,-256S397.4,0 256,0C185.3,0 121.3,28.7 75,75zM256,128c-13.3,0 -24,10.7 -24,24L232,256c0,6.4 2.5,12.5 7,17l72,72c9.4,9.4 24.6,9.4 33.9,0s9.4,-24.6 0,-33.9l-65,-65L279.9,152c0,-13.3 -10.7,-24 -24,-24z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="640dp"
android:height="512dp"
android:viewportWidth="640"
android:viewportHeight="512">
<path
android:fillColor="#FF000000"
android:pathData="M593.8,59.1L46.2,59.1C20.7,59.1 0,79.8 0,105.2v301.5c0,25.5 20.7,46.2 46.2,46.2h547.7c25.5,0 46.2,-20.7 46.1,-46.1L640,105.2c0,-25.4 -20.7,-46.1 -46.2,-46.1zM338.5,360.6L277,360.6v-120l-61.5,76.9 -61.5,-76.9v120L92.3,360.6L92.3,151.4h61.5l61.5,76.9 61.5,-76.9h61.5v209.2zM473.8,363.7L381.5,256L443,256L443,151.4h61.5L504.5,256L566,256z"/>
</vector>

View File

@ -2,16 +2,111 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<!-- NestedScrollingChild goes here (NestedScrollView, RecyclerView, etc.). --> <!-- NestedScrollingChild goes here (NestedScrollView, RecyclerView, etc.). -->
<androidx.core.widget.NestedScrollView <ViewAnimator
android:id="@+id/view_animator"
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.search.SearchBar$ScrollingViewBehavior"> app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<!-- Screen content goes here. --> <!-- Platform content -->
</androidx.core.widget.NestedScrollView> <androidx.core.widget.NestedScrollView
android:id="@+id/platform_content_nested_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:orientation="vertical">
<com.google.android.material.card.MaterialCardView
android:id="@+id/hashtags_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.Material3.CardView.Filled">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<TextView
android:id="@+id/hashtags_card_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hashtags"
android:textSize="18sp"
android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/chip_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/hashtags_card_title">
</com.google.android.material.chip.ChipGroup>
<com.google.android.material.button.MaterialButton
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"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/new_users_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.Material3.CardView.Filled"
android:layout_marginTop="16dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<TextView
android:id="@+id/new_users_card_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/newest_profiles"
android:textSize="18sp"
android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/new_users_recycler_view"
android:layout_width="match_parent"
android:layout_height="200dp"
android:clipChildren="false"
android:clipToPadding="false"
app:layout_constraintTop_toBottomOf="@id/new_users_card_title"/>
<com.google.android.material.button.MaterialButton
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"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<!-- Search results -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view_search_results"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</ViewAnimator>
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -20,7 +115,8 @@
android:id="@+id/search_bar" android:id="@+id/search_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/searchbar_hint" /> android:hint="@string/searchbar_hint"
app:menu="@menu/search_bar_menu"/>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>

View File

@ -0,0 +1,40 @@
<?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="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<ImageView
android:id="@+id/history_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_margin="16dp"
android:src="@drawable/clock_rotate_left_solid"
app:layout_constraintBottom_toBottomOf="@+id/delete_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/delete_button" />
<TextView
android:id="@+id/suggestion_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
app:layout_constraintBottom_toBottomOf="@+id/delete_button"
app:layout_constraintEnd_toStartOf="@id/delete_button"
app:layout_constraintStart_toEndOf="@+id/history_icon"
app:layout_constraintTop_toTopOf="@+id/delete_button"
tools:text="hello search" />
<com.google.android.material.button.MaterialButton
android:id="@+id/delete_button"
style="?attr/materialIconButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/baseline_close_24"
android:layout_margin="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.carousel.MaskableFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="150dp"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:foreground="?attr/selectableItemBackground"
app:shapeAppearance="?attr/shapeAppearanceCornerLarge"
android:layout_margin="8dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/user_carousel_card"
android:layout_width="match_parent"
android:layout_height="match_parent"
style="@style/Widget.Material3.CardView.Elevated">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/user_carousel_image_view"
android:layout_width="match_parent"
android:layout_height="160dp"
android:scaleType="centerCrop"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars"/>
<TextView
android:id="@+id/user_carousel_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="Name"
android:layout_marginHorizontal="8dp"
android:ellipsize="end"
android:textAlignment="center"
android:textColor="@color/on_surface"
android:maxLines="1"
android:layout_marginBottom="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</com.google.android.material.carousel.MaskableFrameLayout>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/close_button"
android:title="@string/close"
android:icon="@drawable/baseline_close_24"
app:showAsAction="always"/>
</menu>

View File

@ -198,4 +198,7 @@
<string name="delete_notification">Delete notification</string> <string name="delete_notification">Delete notification</string>
<string name="delete_notification_dialog_message">Remove this notification?</string> <string name="delete_notification_dialog_message">Remove this notification?</string>
<string name="error_making_request">An error ocurred</string> <string name="error_making_request">An error ocurred</string>
<string name="hashtags">Hashtags</string>
<string name="newest_profiles">Newest profiles</string>
<string name="see_all">See all</string>
</resources> </resources>