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
35 changes: 34 additions & 1 deletion app/src/main/java/com/jerboa/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
Expand All @@ -39,8 +40,11 @@ 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.edit.CommentEditViewModel
import com.jerboa.ui.components.comment.reply.CommentReplyActivity
import com.jerboa.ui.components.comment.reply.CommentReplyViewModel
import com.jerboa.ui.components.comment.reply.ReplyItem
import com.jerboa.ui.components.common.CommentEditDeps
import com.jerboa.ui.components.common.MarkdownHelper
Expand All @@ -55,17 +59,24 @@ 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
import com.jerboa.ui.components.home.sidebar.SiteSidebarActivity
import com.jerboa.ui.components.inbox.InboxActivity
import com.jerboa.ui.components.inbox.InboxViewModel
import com.jerboa.ui.components.login.LoginActivity
import com.jerboa.ui.components.person.PersonProfileActivity
import com.jerboa.ui.components.person.PersonProfileViewModel
import com.jerboa.ui.components.post.PostActivity
import com.jerboa.ui.components.post.create.CreatePostActivity
import com.jerboa.ui.components.post.create.CreatePostViewModel
import com.jerboa.ui.components.post.edit.PostEditActivity
import com.jerboa.ui.components.post.edit.PostEditViewModel
import com.jerboa.ui.components.privatemessage.PrivateMessageReplyActivity
import com.jerboa.ui.components.report.CreateReportViewModel
import com.jerboa.ui.components.report.comment.CreateCommentReportActivity
import com.jerboa.ui.components.report.post.CreatePostReportActivity
import com.jerboa.ui.components.settings.SettingsActivity
Expand All @@ -79,11 +90,27 @@ 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 communityViewModel by viewModels<CommunityViewModel>()
private val personProfileViewModel by viewModels<PersonProfileViewModel>()
private val inboxViewModel by viewModels<InboxViewModel>()
private val communityListViewModel by viewModels<CommunityListViewModel>() {
CommunityListViewModelFactory((application as JerboaApplication).searchHistoryRepository)
wkoomson marked this conversation as resolved.
Show resolved Hide resolved
}
private val createPostViewModel by viewModels<CreatePostViewModel>()
private val commentReplyViewModel by viewModels<CommentReplyViewModel>()
private val commentEditViewModel by viewModels<CommentEditViewModel>()
private val postEditViewModel by viewModels<PostEditViewModel>()
private val createReportViewModel by viewModels<CreateReportViewModel>()
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 @@ -350,6 +377,12 @@ class MainActivity : AppCompatActivity() {
),
) {
val args = Route.CommunityListArgs(it)
// Whenever navigating here, reset the list with your followed communities
SideEffect {
communityListViewModel.setCommunityListFromFollowed(siteViewModel)
communityListViewModel.resetSearch()
}

CommunityListActivity(
navController = navController,
accountViewModel = accountViewModel,
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
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,33 @@
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.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel
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 +36,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 Down Expand Up @@ -69,24 +74,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