Skip to content

Commit 2c63c53

Browse files
committed
feat: implement note list sorting and bump version to 8.5.9
- Add note list sorting functionality supporting 6 sort options across last modified date, creation date, and title (ascending/descending). - Support reactive query sorting in SQLDelight and Room via `NoteDAO.noteSortFlow`. - Introduce `SortField`, `SortOrder`, `NoteSort` domain models, `NoteSortUseCase`, and `PreferencesRepo` using `multiplatform-settings` for persistence. - Add `SortMenuButton` and `SortOption` composables to `MainScreen` top app bar and update `NoteItem` to dynamically render creation and modification timestamps with icons. - Add multiplatform Compose UI tests (`SortNotesTestCase`), database integration tests, ViewModel, and repository unit tests. - Bump application version to 8.5.9 across Android, Desktop, and iOS targets. - Update Gradle wrapper to 9.8.2 and update core dependencies including AGP, KSP, and Firebase.
1 parent 2275dc3 commit 2c63c53

48 files changed

Lines changed: 1004 additions & 77 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [8.5.9] - 2026-11-02
9+
10+
### Features
11+
- Add note list sorting functionality with 6 sort options: by last modified date (descending/ascending), creation date (descending/ascending), and title (A–Z / Z–A)
12+
- Support reactive sorting updates in SQLite queries across both SQLDelight (`Note.sq`) and Room (`@RawQuery`) via `NoteDAO.noteSortFlow`
13+
- Add `SortMenuButton` and `SortOption` to `MainScreen` top app bar with English and Russian localizations
14+
- Add smart date display in `NoteItem`: show single creation timestamp with `CalendarToday` icon when unmodified, or both creation (`AddCircleOutline`) and modification (`Edit`) timestamps side-by-side when modified
15+
- Persist sorting preferences across app restarts via `PreferencesRepo` / `PreferencesRepoImpl` backed by `multiplatform-settings`
16+
17+
### Refactoring
18+
- Centralize note sorting domain models (`NoteSort`, `SortField`, `SortOrder`) and introduce `NoteSortUseCase` to synchronize sort preferences without coupling presentation to persistence
19+
- Optimize and deduplicate SQLDelight `pagedNotes` query by computing sort field values within a subquery
20+
- Place `companion object` at the bottom of Kotlin classes/interfaces across the codebase
21+
- Simplify syntax in DAO, ViewModel, and UI: import companion members directly (`noteSortFlow`), inline redundant single-use variables, and use single-expression bodies for composable wrappers
22+
23+
### Tests
24+
- Add `SortNotesTestCase` multiplatform Compose UI tests covering sorting flows on Android, Desktop, iOS, and Web
25+
- Add `NoteSortingTest` (SQLDelight) and `RoomDatabaseIntegrationTest` (Room) verifying dynamic sorting order
26+
- Add `MainViewModelTest` and `PreferencesRepoImplTest` unit test coverage for sort state management and persistence
27+
28+
### Documentation
29+
- Document code style rules in `CONTRIBUTING.md` for companion object placement, companion member imports, local variable inlining, expression bodies, and component call-site parameter ordering
30+
- Update `docs/ROADMAP.md` (mark Milestone 1.1 §2 Note Sorting complete and add pre-formatted date strings to technical debt)
31+
- Update module `README.md` files for sorting domain models, presentation state, and UI components
32+
33+
### Chores
34+
- Update Gradle wrapper to 9.8.2
35+
- Update dependencies in `libs.versions.toml`: AGP (9.5.0), KSP (2.3.13), Firebase (35.1.0), AndroidX DataStore (1.3.0), Navigation (2.10.0), Tracing (2.0.5), kotlinx-serialization (1.12.0), and SQLCipher for Android (4.20.0)
36+
837
## [8.5.8] - 2026-10-26
938

1039
### Features

‎CONTRIBUTING.md‎

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ kotlin.code.style=official
8484
- No wildcard imports (except in tests)
8585
- Organized and optimized
8686
- Android Studio organize imports: ⌥⌘O (Mac) / Ctrl+Alt+O (Windows)
87+
- **Remove unused imports** promptly; never leave orphaned imports after refactoring.
8788

8889
#### Blank Lines
8990
- One blank line between functions
@@ -120,6 +121,9 @@ kotlin.code.style=official
120121
over an inverted `if (!success) ... else ...` ladder.
121122
- **Collapse trivial `viewModelScope.launch` blocks to a single line** when their body is one statement
122123
(e.g. `private fun cancel() = viewModelScope.launch { router.popBackStack() }`).
124+
- **Inline trivial, single-use local variables** directly into call sites or data class constructors
125+
(e.g. `NoteListResult.Success(..., noteSort = NoteDAO.noteSortFlow.value)`) instead of declaring an
126+
intermediate `val` that is immediately consumed only once.
123127
- **Compose state edits**: prefer a single `mutableStateFlow.update { it.copy(...) }` that sets every field
124128
affected by an event over multiple chained `update` calls. It keeps the resulting state atomic and
125129
makes the visible transition obvious.
@@ -150,30 +154,30 @@ kotlin.code.style=official
150154

151155
```kotlin
152156
class MyClass {
153-
// 1. Companion object
154-
companion object {
155-
const val CONSTANT = "value"
156-
}
157-
158-
// 2. Properties
157+
// 1. Properties
159158
private val privateProperty: String
160159
val publicProperty: String
161160

162-
// 3. Init blocks
161+
// 2. Init blocks
163162
init {
164163
// Initialization
165164
}
166165

167-
// 4. Secondary constructors
166+
// 3. Secondary constructors
168167
constructor(param: String) : this(param, "default")
169168

170-
// 5. Functions (public first, then private)
169+
// 4. Functions (public first, then private)
171170
fun publicFunction() { }
172171

173172
private fun privateFunction() { }
174173

175-
// 6. Nested classes/objects
174+
// 5. Nested classes/objects
176175
inner class NestedClass
176+
177+
// 6. Companion object (at the very bottom of the class or interface)
178+
companion object {
179+
const val CONSTANT = "value"
180+
}
177181
}
178182
```
179183

@@ -280,6 +284,8 @@ private fun ScreenContent(
280284
5. **Remember wisely**: Use `remember` for expensive calculations
281285
6. **Keys in lists**: Provide stable keys for LazyColumn/LazyRow
282286
7. **Adaptive drag handle**: Prefer method references for pane expansion, e.g. `paneExpansionDragHandle = ThreePaneScaffoldScope::VerticalPaneExpansionDragHandle` (use a lambda in Kotlin/Wasm actuals to avoid the current composable function reference compiler issue).
287+
8. **Expression body for single-root composables**: Prefer single-expression syntax (`= Row(...) { ... }`, `= Column(...) { ... }`) for private helper composables or wrappers whose entire body is a single root layout composable, avoiding redundant curly braces and reducing indentation.
288+
9. **Call site parameter ordering for leaf composables**: When calling standard visual leaf composables (such as `Icon`), place `modifier` first among named arguments (e.g. `Icon(modifier = Modifier.size(14.dp), imageVector = ..., contentDescription = ...)`) so that sizing and layout constraints are immediately visible before content properties.
283289

284290
### ViewModel Style
285291

‎app/android/build.gradle.kts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ android {
2626
applicationId = "com.softartdev.noteroom"
2727
minSdk = libs.versions.minSdk.get().toInt()
2828
targetSdk = libs.versions.targetSdk.get().toInt()
29-
versionCode = 858
30-
versionName = "8.5.8"
29+
versionCode = 859
30+
versionName = "8.5.9"
3131
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
3232
testInstrumentationRunnerArguments["clearPackageData"] = "true"
3333
vectorDrawables.useSupportLibrary = true

‎app/android/src/androidTest/java/com/softartdev/notedelight/ui/AndroidUiTests.kt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ class AndroidUiTests : AbstractJvmUiTests() {
7070
@Test
7171
override fun consoleFeatureTest() = super.consoleFeatureTest()
7272

73+
@Test
74+
override fun sortNotesTest() = super.sortNotesTest()
75+
7376
override fun pressBack() = Espresso.pressBack()
7477

7578
override fun closeSoftKeyboard() = Espresso.closeSoftKeyboard()

‎app/desktop/build.gradle.kts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ compose.desktop {
5757
nativeDistributions {
5858
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
5959
packageName = "Note Delight"
60-
packageVersion = "8.5.8"
60+
packageVersion = "8.5.9"
6161
description = "Note app with encryption"
6262
copyright = "© 2023 SoftArtDev"
6363
macOS.iconFile.set(project.file("src/jvmMain/resources/app_icon.icns"))

‎app/desktop/src/jvmTest/kotlin/com/softartdev/notedelight/ui/DesktopUiTests.kt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ class DesktopUiTests : AbstractJvmUiTests() {
104104
@Test
105105
override fun consoleFeatureTest() = super.consoleFeatureTest()
106106

107+
@Test
108+
override fun sortNotesTest() = super.sortNotesTest()
109+
107110
override fun pressBack() {
108111
val backButtons = composeTestRule.onAllNodesWithContentDescription(
109112
label = Icons.AutoMirrored.Filled.ArrowBack.name

‎app/ios-kit/src/commonTest/kotlin/com/softartdev/notedelight/IosUiTests.kt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,7 @@ class IosUiTests : CommonUiTests() {
3838

3939
@Test
4040
override fun localeTest(): TestResult = super.localeTest()
41+
42+
@Test
43+
override fun sortNotesTest(): TestResult = super.sortNotesTest()
4144
}

‎app/iosApp/iosApp.xcodeproj/project.pbxproj‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@
551551
"$(inherited)",
552552
"@executable_path/Frameworks",
553553
);
554-
MARKETING_VERSION = 8.5.8;
554+
MARKETING_VERSION = 8.5.9;
555555
OTHER_LDFLAGS = (
556556
"$(inherited)",
557557
"-ObjC",
@@ -601,7 +601,7 @@
601601
"$(inherited)",
602602
"@executable_path/Frameworks",
603603
);
604-
MARKETING_VERSION = 8.5.8;
604+
MARKETING_VERSION = 8.5.9;
605605
OTHER_LDFLAGS = (
606606
"$(inherited)",
607607
"-ObjC",
@@ -697,7 +697,7 @@
697697
"@executable_path/Frameworks",
698698
"@executable_path/../../Frameworks",
699699
);
700-
MARKETING_VERSION = 8.5.8;
700+
MARKETING_VERSION = 8.5.9;
701701
PRODUCT_BUNDLE_IDENTIFIER = com.softartdev.notedelight.ShareExtension;
702702
PRODUCT_NAME = "$(TARGET_NAME)";
703703
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -728,7 +728,7 @@
728728
"@executable_path/Frameworks",
729729
"@executable_path/../../Frameworks",
730730
);
731-
MARKETING_VERSION = 8.5.8;
731+
MARKETING_VERSION = 8.5.9;
732732
PRODUCT_BUNDLE_IDENTIFIER = com.softartdev.notedelight.ShareExtension;
733733
PRODUCT_NAME = "$(TARGET_NAME)";
734734
PROVISIONING_PROFILE_SPECIFIER = "";

‎app/iosApp/iosApp/Info.plist‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
<key>CFBundlePackageType</key>
3939
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
4040
<key>CFBundleShortVersionString</key>
41-
<string>8.5.8</string>
41+
<string>8.5.9</string>
4242
<key>CFBundleVersion</key>
4343
<string>19</string>
4444
<key>ITSAppUsesNonExemptEncryption</key>

‎app/web/src/wasmJsTest/kotlin/com/softartdev/notedelight/WebUiTests.kt‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import com.softartdev.notedelight.ui.cases.FlowAfterCryptTestCase
2828
import com.softartdev.notedelight.ui.cases.LocaleTestCase
2929
import com.softartdev.notedelight.ui.cases.PrepopulateDbTestCase
3030
import com.softartdev.notedelight.ui.cases.SettingPasswordTestCase
31+
import com.softartdev.notedelight.ui.cases.SortNotesTestCase
3132
import com.softartdev.notedelight.util.kermitLogger
3233
import org.koin.core.context.loadKoinModules
3334
import org.koin.core.context.startKoin
@@ -146,6 +147,12 @@ class WebUiTests {
146147
).invoke()
147148
}
148149

150+
@Test
151+
fun sortNotesTest() = runComposeUiTest {
152+
launchApp(composeUiTest = this@runComposeUiTest)
153+
SortNotesTestCase(composeUiTest = this@runComposeUiTest).invoke()
154+
}
155+
149156
private fun launchApp(composeUiTest: ComposeUiTest) {
150157
val lifecycleOwner = TestLifecycleOwner(initialState = Lifecycle.State.RESUMED)
151158
composeUiTest.setContent {

0 commit comments

Comments
 (0)