From e8e2650fa9ef0a45750b452bbec838c083f646df Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Wed, 11 Mar 2026 07:21:17 +0200 Subject: [PATCH] refactor(search): extract shared code to commons, remove dead code - Extract DateUtils (isLeapYear, dateToUnix, timestampToDate) to deduplicate identical implementations in QueryParser and QuerySerializer - Move SavedSearch data class from desktopApp to commons/search - Replace local formatTimestamp with existing toTimeAgo from commons/util - Remove dead code: resolveAuthorName, _authorSuggestions (never called) - Remove dead code: createOrFilters (never wired into SearchScreen) - Remove unused ICacheProvider dependency from AdvancedSearchBarState Co-Authored-By: Claude Opus 4.6 --- .../commons/search/AdvancedSearchBarState.kt | 17 ----- .../amethyst/commons/search/DateUtils.kt | 71 +++++++++++++++++++ .../amethyst/commons/search/QueryParser.kt | 25 +------ .../commons/search/QuerySerializer.kt | 27 +------ .../amethyst/commons/search/SavedSearch.kt | 28 ++++++++ .../amethyst/desktop/SearchHistoryStore.kt | 8 +-- .../subscriptions/SearchFilterFactory.kt | 13 ---- .../amethyst/desktop/ui/SearchScreen.kt | 4 +- .../desktop/ui/search/SearchResultsList.kt | 32 +-------- 9 files changed, 106 insertions(+), 119 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/DateUtils.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SavedSearch.kt diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt index ba054a801..e46bcef97 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.commons.search import com.vitorpamplona.amethyst.commons.model.User -import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.quartz.nip01Core.core.Event import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -44,7 +43,6 @@ enum class ChangeSource { @OptIn(FlowPreview::class) class AdvancedSearchBarState( - private val cache: ICacheProvider, private val scope: CoroutineScope, private val debounceMs: Long = 300L, ) { @@ -82,10 +80,6 @@ class AdvancedSearchBarState( private val _isSearching = MutableStateFlow(false) val isSearching: StateFlow = _isSearching.asStateFlow() - // Author name suggestions for autocomplete - private val _authorSuggestions = MutableStateFlow>(persistentListOf()) - val authorSuggestions: StateFlow> = _authorSuggestions.asStateFlow() - // Expanded panel state private val _panelExpanded = MutableStateFlow(false) val panelExpanded: StateFlow = _panelExpanded.asStateFlow() @@ -214,15 +208,4 @@ class AdvancedSearchBarState( _noteResults.value = (current + newEvents).sortedByDescending { it.createdAt }.toImmutableList() } } - - // Name resolution for autocomplete - @Suppress("UNCHECKED_CAST") - fun resolveAuthorName(name: String) { - if (name.length < 2) { - _authorSuggestions.value = persistentListOf() - return - } - val results = cache.findUsersStartingWith(name, 5) as List - _authorSuggestions.value = results.toImmutableList() - } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/DateUtils.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/DateUtils.kt new file mode 100644 index 000000000..e7b1d053b --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/DateUtils.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.search + +object DateUtils { + fun isLeapYear(year: Int): Boolean = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) + + fun dateToUnix( + year: Int, + month: Int, + day: Int, + ): Long { + var totalDays = 0L + + for (y in 1970 until year) { + totalDays += if (isLeapYear(y)) 366 else 365 + } + + val daysInMonth = intArrayOf(0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + if (isLeapYear(year)) daysInMonth[2] = 29 + for (m in 1 until month) { + totalDays += daysInMonth[m] + } + + totalDays += (day - 1) + + return totalDays * 86400L + } + + fun timestampToDate(timestamp: Long): String { + var remaining = timestamp + var year = 1970 + while (true) { + val daysInYear = if (isLeapYear(year)) 366L else 365L + val secondsInYear = daysInYear * 86400L + if (remaining < secondsInYear) break + remaining -= secondsInYear + year++ + } + + val daysInMonth = intArrayOf(0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + if (isLeapYear(year)) daysInMonth[2] = 29 + + var dayOfYear = (remaining / 86400).toInt() + 1 + var month = 1 + while (month <= 12 && dayOfYear > daysInMonth[month]) { + dayOfYear -= daysInMonth[month] + month++ + } + + return "$year-${month.toString().padStart(2, '0')}-${dayOfYear.toString().padStart(2, '0')}" + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParser.kt index be1a93e34..073e9cc41 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParser.kt @@ -319,28 +319,5 @@ object QueryParser { year: Int, month: Int, day: Int, - ): Long { - // Simple UTC date to unix timestamp calculation - // Days from epoch (1970-01-01) to the given date - var totalDays = 0L - - // Add days for full years - for (y in 1970 until year) { - totalDays += if (isLeapYear(y)) 366 else 365 - } - - // Add days for full months in target year - val daysInMonth = intArrayOf(0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - if (isLeapYear(year)) daysInMonth[2] = 29 - for (m in 1 until month) { - totalDays += daysInMonth[m] - } - - // Add remaining days - totalDays += (day - 1) - - return totalDays * 86400L - } - - private fun isLeapYear(year: Int): Boolean = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) + ): Long = DateUtils.dateToUnix(year, month, day) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializer.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializer.kt index dd7b0612a..3cd27a017 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializer.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializer.kt @@ -84,30 +84,5 @@ object QuerySerializer { return parts.joinToString(" ") } - fun timestampToDate(timestamp: Long): String { - // Convert unix timestamp to YYYY-MM-DD - var remaining = timestamp - var year = 1970 - while (true) { - val daysInYear = if (isLeapYear(year)) 366L else 365L - val secondsInYear = daysInYear * 86400L - if (remaining < secondsInYear) break - remaining -= secondsInYear - year++ - } - - val daysInMonth = intArrayOf(0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - if (isLeapYear(year)) daysInMonth[2] = 29 - - var dayOfYear = (remaining / 86400).toInt() + 1 - var month = 1 - while (month <= 12 && dayOfYear > daysInMonth[month]) { - dayOfYear -= daysInMonth[month] - month++ - } - - return "$year-${month.toString().padStart(2, '0')}-${dayOfYear.toString().padStart(2, '0')}" - } - - private fun isLeapYear(year: Int): Boolean = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) + fun timestampToDate(timestamp: Long): String = DateUtils.timestampToDate(timestamp) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SavedSearch.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SavedSearch.kt new file mode 100644 index 000000000..20302fba7 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SavedSearch.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.search + +data class SavedSearch( + val id: String, + val label: String, + val query: SearchQuery, + val createdAt: Long, +) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/SearchHistoryStore.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/SearchHistoryStore.kt index 8271c1e67..9c3e3b01a 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/SearchHistoryStore.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/SearchHistoryStore.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.desktop import com.vitorpamplona.amethyst.commons.search.QueryParser import com.vitorpamplona.amethyst.commons.search.QuerySerializer +import com.vitorpamplona.amethyst.commons.search.SavedSearch import com.vitorpamplona.amethyst.commons.search.SearchQuery import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -134,10 +135,3 @@ object SearchHistoryStore { prefs.put(KEY_SAVED, raw) } } - -data class SavedSearch( - val id: String, - val label: String, - val query: SearchQuery, - val createdAt: Long, -) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SearchFilterFactory.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SearchFilterFactory.kt index 7aa491f71..6a61b000d 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SearchFilterFactory.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SearchFilterFactory.kt @@ -128,19 +128,6 @@ object SearchFilterFactory { } } - fun createOrFilters( - query: SearchQuery, - limit: Int = 100, - ): List> { - if (query.orTerms.isEmpty()) return listOf(createFilters(query, limit)) - - // Each OR term gets its own set of filters - return query.orTerms.take(3).map { term -> - val termQuery = query.copy(text = term, orTerms = kotlinx.collections.immutable.persistentListOf()) - createFilters(termQuery, limit) - } - } - private fun buildSearchString(query: SearchQuery): String? { val parts = mutableListOf() diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt index d7fd9a643..885b48843 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt @@ -111,7 +111,7 @@ fun SearchScreen( modifier: Modifier = Modifier, ) { val scope = rememberCoroutineScope() - val state = remember { AdvancedSearchBarState(localCache, scope) } + val state = remember { AdvancedSearchBarState(scope) } val focusRequester = remember { FocusRequester() } // Pre-fill initial query @@ -408,7 +408,7 @@ fun SearchScreen( @Composable private fun SearchEmptyState( historyItems: List, - savedSearches: List, + savedSearches: List, onLoadQuery: (com.vitorpamplona.amethyst.commons.search.SearchQuery) -> Unit, onDeleteSaved: (String) -> Unit, onClearHistory: () -> Unit, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt index e26d6a462..93a62e39e 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt @@ -62,6 +62,7 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.search.AdvancedSearchBarState import com.vitorpamplona.amethyst.commons.search.KindRegistry import com.vitorpamplona.amethyst.commons.ui.components.UserSearchCard +import com.vitorpamplona.amethyst.commons.util.toTimeAgo import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent @@ -255,7 +256,7 @@ private fun NotePreviewCard( Spacer(Modifier.weight(1f)) // Timestamp Text( - formatTimestamp(event.createdAt), + event.createdAt.toTimeAgo(withDot = false), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -296,32 +297,3 @@ private fun ExpandableSection( } } } - -private fun formatTimestamp(ts: Long): String { - val now = System.currentTimeMillis() / 1000 - val diff = now - ts - return when { - diff < 60 -> { - "just now" - } - - diff < 3600 -> { - "${diff / 60}m ago" - } - - diff < 86400 -> { - "${diff / 3600}h ago" - } - - diff < 604800 -> { - "${diff / 86400}d ago" - } - - else -> { - // Simple date format - val days = ts / 86400 - val year = 1970 + (days / 365).toInt() - "$year" - } - } -}