Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add local search history to the community list screen #871

Closed
wants to merge 13 commits into from
16 changes: 14 additions & 2 deletions app/src/main/java/com/jerboa/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ import com.jerboa.db.AppDB
import com.jerboa.db.AppSettingsRepository
import com.jerboa.db.AppSettingsViewModel
import com.jerboa.db.AppSettingsViewModelFactory
import com.jerboa.db.SearchHistoryRepository
import com.jerboa.db.SearchHistoryViewModel
import com.jerboa.db.SearchHistoryViewModelFactory
import com.jerboa.model.AccountSettingsViewModel
import com.jerboa.model.AccountSettingsViewModelFactory
import com.jerboa.model.CommunityViewModel
Expand Down Expand Up @@ -90,7 +93,10 @@ import com.jerboa.util.ShowConfirmationDialog
class JerboaApplication : Application() {
private val database by lazy { AppDB.getDatabase(this) }
val accountRepository by lazy { AccountRepository(database.accountDao()) }
val appSettingsRepository by lazy { AppSettingsRepository(database.appSettingsDao()) }
val appSettingsRepository by lazy {
AppSettingsRepository(database.appSettingsDao(), database.searchHistoryDao())
}
val searchHistoryRepository by lazy { SearchHistoryRepository(database.searchHistoryDao()) }
}

class MainActivity : AppCompatActivity() {
Expand All @@ -104,6 +110,9 @@ class MainActivity : AppCompatActivity() {
private val appSettingsViewModel: AppSettingsViewModel by viewModels {
AppSettingsViewModelFactory((application as JerboaApplication).appSettingsRepository)
}
private val searchHistoryViewModel: SearchHistoryViewModel by viewModels {
SearchHistoryViewModelFactory((application as JerboaApplication).searchHistoryRepository)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand Down Expand Up @@ -231,6 +240,7 @@ class MainActivity : AppCompatActivity() {
accountViewModel = accountViewModel,
siteViewModel = siteViewModel,
appSettingsViewModel = appSettingsViewModel,
searchHistoryViewModel = searchHistoryViewModel,
appSettings = appSettings,
drawerState = drawerState,
)
Expand Down Expand Up @@ -413,10 +423,12 @@ class MainActivity : AppCompatActivity() {
CommunityListActivity(
navController = navController,
accountViewModel = accountViewModel,
siteViewModel = siteViewModel,
selectMode = args.select,
siteViewModel = siteViewModel,
blurNSFW = appSettings.blurNSFW,
drawerState = drawerState,
appSettingsViewModel = appSettingsViewModel,
searchHistoryViewModel = searchHistoryViewModel,
)
}

Expand Down
85 changes: 83 additions & 2 deletions app/src/main/java/com/jerboa/db/AppDB.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import androidx.room.*
import androidx.sqlite.db.SupportSQLiteDatabase
Expand Down Expand Up @@ -131,6 +132,11 @@ data class AppSettings(
defaultValue = "1",
)
val backConfirmationMode: Int,
@ColumnInfo(
name = "save_search_history",
defaultValue = "1",
)
val saveSearchHistory: Boolean,
)

val APP_SETTINGS_DEFAULT = AppSettings(
Expand All @@ -152,6 +158,22 @@ val APP_SETTINGS_DEFAULT = AppSettings(
blurNSFW = true,
showTextDescriptionsInNavbar = true,
backConfirmationMode = 1,
saveSearchHistory = true,
)

@Entity(
foreignKeys = [ForeignKey(
entity = Account::class,
parentColumns = ["id"],
childColumns = ["account_id"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE,
)],
)
data class SearchHistory(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
@ColumnInfo(name = "account_id", index = true) val accountId: Int?,
@ColumnInfo(name = "search_term",) val searchTerm: String,
)

@Dao
Expand Down Expand Up @@ -193,6 +215,21 @@ interface AppSettingsDao {
suspend fun updatePostViewMode(postViewMode: Int)
}

@Dao
interface SearchHistoryDao {
@Query("SELECT * FROM SearchHistory")
fun history(): LiveData<List<SearchHistory>>

@Insert(onConflict = OnConflictStrategy.REPLACE, entity = SearchHistory::class)
suspend fun insert(item: SearchHistory)

@Query("DELETE FROM SearchHistory WHERE account_id = :accountId AND search_term = :searchTerm")
suspend fun delete(accountId: Int?, searchTerm: String)

@Query("DELETE FROM SearchHistory")
suspend fun clear()
}

// Declares the DAO as a private property in the constructor. Pass in the DAO
// instead of the whole database, because you only need access to the DAO
class AccountRepository(private val accountDao: AccountDao) {
Expand Down Expand Up @@ -238,6 +275,7 @@ class AccountRepository(private val accountDao: AccountDao) {
// instead of the whole database, because you only need access to the DAO
class AppSettingsRepository(
private val appSettingsDao: AppSettingsDao,
private val searchHistoryDao: SearchHistoryDao,
private val httpClient: OkHttpClient = OkHttpClient(),
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) {
Expand All @@ -252,6 +290,9 @@ class AppSettingsRepository(
@WorkerThread
suspend fun update(appSettings: AppSettings) {
appSettingsDao.updateAppSettings(appSettings)
if (!appSettings.saveSearchHistory) {
searchHistoryDao.clear()
}
}

@WorkerThread
Expand Down Expand Up @@ -282,14 +323,31 @@ class AppSettingsRepository(
}
}

class SearchHistoryRepository(
private val searchHistoryDao: SearchHistoryDao,
) {
fun history(): LiveData<List<SearchHistory>> = searchHistoryDao.history()
.map { history ->
history
.sortedByDescending { it.id }
.distinctBy { it.searchTerm }
}

suspend fun insert(item: SearchHistory) =
searchHistoryDao.insert(item)

suspend fun delete(item: SearchHistory) = searchHistoryDao.delete(item.accountId, item.searchTerm)
}

@Database(
version = 19,
entities = [Account::class, AppSettings::class],
version = 20,
entities = [Account::class, AppSettings::class, SearchHistory::class],
exportSchema = true,
)
abstract class AppDB : RoomDatabase() {
abstract fun accountDao(): AccountDao
abstract fun appSettingsDao(): AppSettingsDao
abstract fun searchHistoryDao(): SearchHistoryDao

companion object {
@Volatile
Expand Down Expand Up @@ -399,3 +457,26 @@ class AppSettingsViewModelFactory(private val repository: AppSettingsRepository)
throw IllegalArgumentException("Unknown ViewModel class")
}
}

class SearchHistoryViewModel(private val repository: SearchHistoryRepository) : ViewModel() {
val searchHistory = repository.history()

suspend fun insert(item: SearchHistory) {
repository.insert(item)
}

suspend fun delete(item: SearchHistory) {
repository.delete(item)
}
}

class SearchHistoryViewModelFactory(private val repository: SearchHistoryRepository) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(SearchHistoryViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return SearchHistoryViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
26 changes: 26 additions & 0 deletions app/src/main/java/com/jerboa/db/Migrations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,31 @@ val MIGRATION_18_19 = object : Migration(18, 19) {
}
}

val MIGRATION_19_20 = object : Migration(19, 20) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(UPDATE_APP_CHANGELOG_UNVIEWED)
database.execSQL(
"ALTER TABLE AppSettings ADD COLUMN save_search_history INTEGER NOT NULL DEFAULT 1",
)
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS SearchHistory(
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
account_id INTEGER,
search_term TEXT NOT NULL,
FOREIGN KEY(account_id)
REFERENCES Account(id)
ON DELETE CASCADE
ON UPDATE CASCADE
)
""",
)
database.execSQL(
"CREATE INDEX index_SearchHistory_account_id ON SearchHistory(account_id)"
)
}
}

// Don't forget to test your migration with `./gradlew app:connectAndroidTest`
val MIGRATIONS_LIST = arrayOf(
MIGRATION_1_2,
Expand All @@ -240,4 +265,5 @@ val MIGRATIONS_LIST = arrayOf(
MIGRATION_16_17,
MIGRATION_17_18,
MIGRATION_18_19,
MIGRATION_19_20,
)
18 changes: 7 additions & 11 deletions app/src/main/java/com/jerboa/model/CommunityListViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class CommunityListViewModel : ViewModel(), Initializable {
var searchRes: ApiState<SearchResponse> by mutableStateOf(ApiState.Empty)
private set

var communities: List<CommunityView> by mutableStateOf(emptyList())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should match main, looks like it was a wrong conflict fix.


fun searchCommunities(form: Search) {
viewModelScope.launch {
searchRes = ApiState.Loading
Expand All @@ -38,7 +40,7 @@ class CommunityListViewModel : ViewModel(), Initializable {
val myFollows = myUserInfo.follows

// A hack to convert communityFollowerView into CommunityView
val followsIntoCommunityViews = myFollows.map { cfv ->
communities = myFollows.map { cfv ->
CommunityView(
community = cfv.community,
subscribed = SubscribedType.Subscribed,
Expand All @@ -57,20 +59,14 @@ class CommunityListViewModel : ViewModel(), Initializable {
),
)
}

searchRes = ApiState.Success(
SearchResponse(
type_ = SearchType.Communities,
communities = followsIntoCommunityViews,
comments = emptyList(),
posts = emptyList(),
users = emptyList(),
),
)
}
}

else -> {}
}
}

fun resetSearch() {
searchRes = ApiState.Empty
}
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,45 @@
package com.jerboa.ui.components.community.list

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewmodel.compose.viewModel
import com.jerboa.R
import com.jerboa.datatypes.sampleCommunityView
import com.jerboa.datatypes.types.*
import com.jerboa.db.SearchHistory
import com.jerboa.db.SearchHistoryViewModel
import com.jerboa.model.CommunityListViewModel
import com.jerboa.ui.components.common.simpleVerticalScrollbar
import com.jerboa.ui.components.community.CommunityLinkLarger
import com.jerboa.ui.components.community.CommunityLinkLargerWithUserCount
import kotlinx.coroutines.launch

@OptIn(ExperimentalMaterial3Api::class)
@Composable
Expand Down Expand Up @@ -161,3 +174,51 @@ fun SearchViewPreview() {
onSearchChange = {},
)
}

@Composable
fun SearchHistoryList(
history: List<SearchHistory>,
onHistoryItemClicked: (SearchHistory) -> Unit,
onHistoryItemDeleted: (SearchHistory) -> Unit,
) {
Column {
if (history.isNotEmpty()) {
ListItem(
headlineContent = {
Text(
text = stringResource(R.string.community_list_recent_searches),
color = MaterialTheme.colorScheme.onBackground,
style = MaterialTheme.typography.labelLarge,
)
},
)
}
history.forEach {
ListItem(
modifier = Modifier.clickable { onHistoryItemClicked(it) },
headlineContent = {
Text(
text = it.searchTerm,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge,
)
},
trailingContent = {
IconButton(
onClick = { onHistoryItemDeleted(it) },
content = {
Icon(
Icons.Rounded.Close,
contentDescription = stringResource(
R.string.community_list_delete_search_item,
it.searchTerm,
),
tint = MaterialTheme.colorScheme.surfaceTint,
)
},
)
},
)
}
}
}
Loading