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=".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=".hashtags.ui.HashtagsActivity" android:theme="@style/Theme.Isolaatti" />
<provider
android:authorities="com.isolaatti.provider"
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.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 fun keyValueDao(): KeyValueDao
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
import com.isolaatti.connectivity.RetrofitClient
import com.isolaatti.database.AppDatabase
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.Provides
import dagger.hilt.InstallIn
@ -14,4 +18,14 @@ class Module {
fun provideSearchApi(retrofitClient: RetrofitClient): SearchApi {
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
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
interface SearchApi {
@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.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
@Dao
interface SearchDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertTerm(searchTerm: SearchHistoryEntity)
@Delete
fun deleteTerm(searchTerm: SearchHistoryEntity)
@Query("SELECT COUNT(*) FROM search_history WHERE `query` = :query")
fun itemCount(query: String): Int
@Query("SELECT * FROM search_history ORDER BY time DESC")
fun getTerms(): LiveData<List<SearchHistoryEntity>>
@Transaction
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
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Fts4
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(tableName = "search_history")
@Fts4
data class SearchHistoryEntity(
@PrimaryKey val query: String,
@PrimaryKey
@ColumnInfo(name = "rowid")
val rowId: Int,
val query: String,
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
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 kotlinx.coroutines.flow.Flow
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
import androidx.lifecycle.LiveData
import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.isolaatti.search.data.SearchApi
import com.isolaatti.search.data.SearchDao
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.domain.SearchRepository
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 SearchViewModel @Inject constructor() : ViewModel() {
fun search(query: String) {
class SearchViewModel @Inject constructor(private val searchRepository: SearchRepository) : ViewModel() {
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 {
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
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment
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.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.UserCarouselAdapter
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class SearchFragment : Fragment() {
companion object {
const val LOG_TAG = "SearchFragment"
}
lateinit var viewBinding: FragmentSearchBinding
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(
inflater: LayoutInflater,
@ -24,23 +81,83 @@ class SearchFragment : Fragment() {
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?) {
super.onViewCreated(view, savedInstanceState)
setupListeners()
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() {
viewBinding.searchView.editText.doAfterTextChanged { searchText ->
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() {
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,17 +2,112 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- NestedScrollingChild goes here (NestedScrollView, RecyclerView, etc.). -->
<androidx.core.widget.NestedScrollView
<ViewAnimator
android:id="@+id/view_animator"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.search.SearchBar$ScrollingViewBehavior">
<!-- Screen content goes here. -->
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<!-- Platform content -->
<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
android:layout_width="match_parent"
android:layout_height="wrap_content">
@ -20,7 +115,8 @@
android:id="@+id/search_bar"
android:layout_width="match_parent"
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>

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_dialog_message">Remove this notification?</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>