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: 15 additions & 1 deletion app/src/main/java/com/jerboa/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ 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.ui.components.comment.edit.CommentEditActivity
import com.jerboa.ui.components.comment.reply.CommentReplyActivity
import com.jerboa.ui.components.comment.reply.ReplyItem
Expand All @@ -55,6 +56,8 @@ import com.jerboa.ui.components.common.takeDepsFromRoot
import com.jerboa.ui.components.community.CommunityActivity
import com.jerboa.ui.components.community.CommunityViewModel
import com.jerboa.ui.components.community.list.CommunityListActivity
import com.jerboa.ui.components.community.list.CommunityListViewModel
import com.jerboa.ui.components.community.list.CommunityListViewModelFactory
import com.jerboa.ui.components.community.sidebar.CommunitySidebarActivity
import com.jerboa.ui.components.home.BottomNavActivity
import com.jerboa.ui.components.home.SiteViewModel
Expand All @@ -79,11 +82,19 @@ import com.jerboa.ui.theme.JerboaTheme
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(), database.appSettingsDao())
}
}

class MainActivity : AppCompatActivity() {
private val siteViewModel by viewModels<SiteViewModel>()
private val communityListViewModel by viewModels<CommunityListViewModel>() {
CommunityListViewModelFactory((application as JerboaApplication).searchHistoryRepository)
wkoomson marked this conversation as resolved.
Show resolved Hide resolved
}
private val accountSettingsViewModel by viewModels<AccountSettingsViewModel> {
AccountSettingsViewModelFactory((application as JerboaApplication).accountRepository)
}
Expand Down Expand Up @@ -177,6 +188,7 @@ class MainActivity : AppCompatActivity() {
siteViewModel = siteViewModel,
appSettingsViewModel = appSettingsViewModel,
appSettings = appSettings,
communityListViewModel = communityListViewModel,
)
}

Expand Down Expand Up @@ -350,9 +362,11 @@ class MainActivity : AppCompatActivity() {
),
) {
val args = Route.CommunityListArgs(it)

CommunityListActivity(
navController = navController,
accountViewModel = accountViewModel,
communityListViewModel = communityListViewModel,
siteViewModel = siteViewModel,
selectMode = args.select,
blurNSFW = appSettings.blurNSFW,
Expand Down
72 changes: 70 additions & 2 deletions app/src/main/java/com/jerboa/db/AppDB.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrl
Expand Down Expand Up @@ -123,6 +125,11 @@ data class AppSettings(
defaultValue = "1",
)
val blurNSFW: Boolean,
@ColumnInfo(
name = "save_search_history",
defaultValue = "1",
)
val saveSearchHistory: Boolean,
)

val APP_SETTINGS_DEFAULT = AppSettings(
Expand All @@ -142,8 +149,30 @@ val APP_SETTINGS_DEFAULT = AppSettings(
usePrivateTabs = false,
secureWindow = false,
blurNSFW = true,
saveSearchHistory = true,
)

@Entity
data class SearchHistory(
@PrimaryKey @ColumnInfo(name = "text") val text: String,
wkoomson marked this conversation as resolved.
Show resolved Hide resolved
@ColumnInfo(name = "timestamp") val timestamp: Long,
wkoomson marked this conversation as resolved.
Show resolved Hide resolved
)

@Dao
interface SearchHistoryDao {
@Query("SELECT * FROM SearchHistory")
fun history(): Flow<List<SearchHistory>>
wkoomson marked this conversation as resolved.
Show resolved Hide resolved

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

@Delete(entity = SearchHistory::class)
suspend fun delete(item: SearchHistory)

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

@Dao
interface AccountDao {
@Query("SELECT * FROM account")
Expand Down Expand Up @@ -173,6 +202,9 @@ interface AppSettingsDao {
@Query("SELECT * FROM AppSettings limit 1")
fun getSettings(): LiveData<AppSettings>

@Query("SELECT * FROM AppSettings limit 1")
wkoomson marked this conversation as resolved.
Show resolved Hide resolved
fun settings(): Flow<AppSettings>

@Update
suspend fun updateAppSettings(appSettings: AppSettings)

Expand Down Expand Up @@ -224,10 +256,26 @@ class AccountRepository(private val accountDao: AccountDao) {
}
}

class SearchHistoryRepository(
private val searchHistoryDao: SearchHistoryDao,
private val appSettingsDao: AppSettingsDao,
) {
fun history(): Flow<List<SearchHistory>> = searchHistoryDao.history()

suspend fun insert(item: SearchHistory) {
if (appSettingsDao.settings().first().saveSearchHistory) {
wkoomson marked this conversation as resolved.
Show resolved Hide resolved
searchHistoryDao.insert(item)
}
}

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

// 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 AppSettingsRepository(
private val appSettingsDao: AppSettingsDao,
private val searchHistoryDao: SearchHistoryDao,
private val httpClient: OkHttpClient = OkHttpClient(),
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) {
Expand All @@ -242,6 +290,7 @@ class AppSettingsRepository(
@WorkerThread
suspend fun update(appSettings: AppSettings) {
appSettingsDao.updateAppSettings(appSettings)
if (!appSettings.saveSearchHistory) searchHistoryDao.clear()
wkoomson marked this conversation as resolved.
Show resolved Hide resolved
}

@WorkerThread
Expand Down Expand Up @@ -469,14 +518,32 @@ val MIGRATION_16_17 = object : Migration(16, 17) {
}
}

val MIGRATION_17_18 = object : Migration(17, 18) {
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(
history TEXT PRIMARY KEY NOT NULL,
wkoomson marked this conversation as resolved.
Show resolved Hide resolved
timestamp INTEGER NOT NULL
)
""",
)
}
}

@Database(
version = 17,
entities = [Account::class, AppSettings::class],
version = 18,
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 @@ -511,6 +578,7 @@ abstract class AppDB : RoomDatabase() {
MIGRATION_14_15,
MIGRATION_15_16,
MIGRATION_16_17,
MIGRATION_17_18,
)
// Necessary because it can't insert data on creation
.addCallback(object : Callback() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
package com.jerboa.ui.components.community.list

import android.util.Log
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import com.jerboa.DEBOUNCE_DELAY
import com.jerboa.R
import com.jerboa.api.ApiState
import com.jerboa.datatypes.types.Search
import com.jerboa.datatypes.types.SearchType
import com.jerboa.datatypes.types.SortType
import com.jerboa.db.AccountViewModel
import com.jerboa.ui.components.common.ApiEmptyText
import com.jerboa.ui.components.common.ApiErrorText
import com.jerboa.ui.components.common.InitializeRoute
import com.jerboa.ui.components.common.LoadingBar
Expand All @@ -30,7 +35,6 @@ import com.jerboa.ui.components.common.getCurrentAccount
import com.jerboa.ui.components.common.toCommunity
import com.jerboa.ui.components.home.SiteViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

private var fetchCommunitiesJob: Job? = null
Expand All @@ -43,6 +47,7 @@ object CommunityListReturn {
fun CommunityListActivity(
navController: NavController,
accountViewModel: AccountViewModel,
communityListViewModel: CommunityListViewModel,
selectMode: Boolean = false,
siteViewModel: SiteViewModel,
blurNSFW: Boolean,
Expand All @@ -51,7 +56,6 @@ fun CommunityListActivity(

val account = getCurrentAccount(accountViewModel = accountViewModel)

val communityListViewModel: CommunityListViewModel = viewModel()
InitializeRoute(communityListViewModel) {
// Whenever navigating here, reset the list with your followed communities
communityListViewModel.setCommunityListFromFollowed(siteViewModel)
Expand All @@ -69,24 +73,100 @@ fun CommunityListActivity(
search = search,
onSearchChange = {
search = it
fetchCommunitiesJob?.cancel()
fetchCommunitiesJob = scope.launch {
delay(DEBOUNCE_DELAY)
communityListViewModel.searchCommunities(
form = Search(
q = search,
type_ = SearchType.Communities,
sort = SortType.TopAll,
auth = account?.jwt,
),
)
if (it.isEmpty()) {
communityListViewModel.resetSearch()
return@CommunityListHeader
}
scope.launch {
communityListViewModel.searchAllCommunities(search, account?.jwt, true)
}
},
)
},
content = { padding ->
when (val communitiesRes = communityListViewModel.searchRes) {
ApiState.Empty -> ApiEmptyText()
ApiState.Empty -> {
Column(
modifier = Modifier
.padding(padding)
.imePadding(),
) {
val history by communityListViewModel.searchHistory.collectAsState(
emptyList(),
)
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 {
scope.launch {
search = it.text
communityListViewModel.searchAllCommunities(
it.text,
account?.jwt,
)
}
},
headlineContent = {
Text(
text = it.text,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyLarge,
)
},
trailingContent = {
IconButton(
onClick = {
scope.launch {
communityListViewModel.deleteSearchHistory(it)
}
},
content = {
Icon(
Icons.Rounded.Close,
contentDescription = stringResource(
R.string.community_list_delete_search_item,
it.text,
),
tint = MaterialTheme.colorScheme.surfaceTint,
)
},
)
},
)
}
ListItem(
headlineContent = {
Text(
text = stringResource(R.string.community_list_title),
color = MaterialTheme.colorScheme.onBackground,
style = MaterialTheme.typography.labelLarge,
)
},
)
CommunityListings(
communities = communityListViewModel.communities,
onClickCommunity = { cs ->
if (selectMode) {
communityListViewModel.selectCommunity(cs)
navController.navigateUp()
} else {
navController.navigate(route = "community/${cs.id}")
}
},
blurNSFW = blurNSFW,
)
}
}
is ApiState.Failure -> ApiErrorText(communitiesRes.msg)
ApiState.Loading -> {
LoadingBar(padding)
Expand Down
Loading