From 25a5c51b0a85da189afb4180be3c521d8414798e Mon Sep 17 00:00:00 2001 From: Avinal Kumar Date: Thu, 21 May 2026 17:25:57 +0530 Subject: [PATCH] Fix 6 bugs: wire settings, dedup toggle, timeouts, leak, react 1. Default visibility now used in compose card (reads from DataStore) 2. Week start day wired to explorer calendar (rotates day labels, adjusts Zeller offset) 3. Task toggle deduplicated: both MemoListViewModel and MemoDetailViewModel now use TaskParser.toggleTaskInContent instead of manual string replacement 4. reactToMemo refetches single memo instead of all memos 5. AppDependencies stores Job reference, cancels before re-init 6. HTTP timeouts added: 30s request/socket, 15s connect 102 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Avinal Kumar --- .../kotlin/com/avinal/memos/AppDependencies.kt | 5 ++++- .../com/avinal/memos/api/HttpClientFactory.kt | 5 +++++ .../com/avinal/memos/domain/MemoRepository.kt | 8 +++++++- .../com/avinal/memos/ui/memos/MainScreen.kt | 10 ++++++---- .../avinal/memos/ui/memos/MemoDetailViewModel.kt | 15 ++++----------- .../com/avinal/memos/ui/memos/MemoListScreen.kt | 5 ++++- .../avinal/memos/ui/memos/MemoListViewModel.kt | 13 ++++--------- 7 files changed, 34 insertions(+), 27 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/AppDependencies.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/AppDependencies.kt index 2358341..b1a3510 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/AppDependencies.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/AppDependencies.kt @@ -39,8 +39,11 @@ class AppDependencies( val authRepository: AuthRepository by lazy { AuthRepository(apiClient, tokenStore) } val memoRepository: MemoRepository by lazy { MemoRepository(apiClient, database.memoDao()) } + private var initJob: kotlinx.coroutines.Job? = null + fun initialize() { - CoroutineScope(Dispatchers.IO).launch { + initJob?.cancel() + initJob = CoroutineScope(Dispatchers.IO).launch { launch { tokenStore.accessToken.collect { cachedToken = it } } launch { tokenStore.serverUrl.collect { cachedServerUrl = it } } } diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/api/HttpClientFactory.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/api/HttpClientFactory.kt index 413eb5c..3992db7 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/api/HttpClientFactory.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/api/HttpClientFactory.kt @@ -19,6 +19,11 @@ object HttpClientFactory { encodeDefaults = true }) } + install(io.ktor.client.plugins.HttpTimeout) { + requestTimeoutMillis = 30_000 + connectTimeoutMillis = 15_000 + socketTimeoutMillis = 30_000 + } } client.plugin(HttpSend).intercept { request -> diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/domain/MemoRepository.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/domain/MemoRepository.kt index d6fa0d2..c0e3a02 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/domain/MemoRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/domain/MemoRepository.kt @@ -141,7 +141,13 @@ class MemoRepository( suspend fun reactToMemo(memoId: String, emoji: String): ApiResult { return when (apiClient.upsertReaction(memoId, emoji)) { is ApiResult.Success -> { - refreshMemos() + when (val memoResult = apiClient.getMemo(memoId)) { + is ApiResult.Success -> { + val memo = memoResult.data.toDomain() + memoDao.upsert(memo.toEntity(nowMillis())) + } + else -> {} + } ApiResult.Success(Unit) } is ApiResult.Error -> ApiResult.Error(0, "Failed to react") diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MainScreen.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MainScreen.kt index 63bc120..3b026df 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MainScreen.kt @@ -276,7 +276,9 @@ private fun ExplorerPage( Spacer(Modifier.height(8.dp)) - val dayLabels = listOf("m", "t", "w", "t", "f", "s", "s") + val weekStartDay by deps.tokenStore.weekStartDay.collectAsState(initial = 0) + val baseDays = listOf("s", "m", "t", "w", "t", "f", "s") + val dayLabels = baseDays.drop(weekStartDay) + baseDays.take(weekStartDay) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { dayLabels.forEach { day -> Text(day, fontSize = 11.sp, color = subtleColor.copy(alpha = 0.5f), modifier = Modifier.width(28.dp), textAlign = TextAlign.Center) @@ -284,13 +286,13 @@ private fun ExplorerPage( } Spacer(Modifier.height(4.dp)) - val firstDayOfWeek = remember(calYear, calMonthIdx) { + val firstDayOfWeek = remember(calYear, calMonthIdx, weekStartDay) { val month = calMonthIdx + 1 val y = if (month <= 2) calYear - 1 else calYear val m = if (month <= 2) month + 12 else month val h = (1 + (13 * (m + 1)) / 5 + y + y / 4 - y / 100 + y / 400) % 7 - val mondayBased = ((h + 5) % 7) - if (mondayBased < 0) mondayBased + 7 else mondayBased + val adjusted = ((h + 6 - weekStartDay) % 7) + if (adjusted < 0) adjusted + 7 else adjusted } val cells = buildList { diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoDetailViewModel.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoDetailViewModel.kt index 2bffded..1ed3746 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoDetailViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoDetailViewModel.kt @@ -37,18 +37,11 @@ class MemoDetailViewModel( fun toggleTask(lineIndex: Int, checked: Boolean) { val current = memo.value ?: return - val lines = current.content.lines().toMutableList() - if (lineIndex !in lines.indices) return - - val line = lines[lineIndex] - lines[lineIndex] = if (checked) { - line.replaceFirst("- [ ]", "- [x]") - } else { - line.replaceFirst("- [x]", "- [ ]").replaceFirst("- [X]", "- [ ]") - } - + val tasks = com.avinal.memos.parser.TaskParser.extractTasks(memoId, current.content) + val task = tasks.find { it.lineIndex == lineIndex } ?: return viewModelScope.launch { - memoRepository.updateMemo(memoId, content = lines.joinToString("\n")) + val newContent = com.avinal.memos.parser.TaskParser.toggleTaskInContent(current.content, task) + if (newContent != current.content) memoRepository.updateMemo(memoId, content = newContent) } } diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListScreen.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListScreen.kt index 60b8939..c6735d3 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListScreen.kt @@ -100,7 +100,10 @@ fun MemoListScreen( val subtleColor = MaterialTheme.colorScheme.onSurfaceVariant var composeText by remember { mutableStateOf("") } - var composeVisibility by remember { mutableStateOf(MemoVisibility.PRIVATE) } + val defaultVis by produceState(MemoVisibility.PRIVATE) { + deps.tokenStore.defaultVisibility.first().let { value = MemoVisibility.fromApiString(it) } + } + var composeVisibility by remember(defaultVis) { mutableStateOf(defaultVis) } var showVisibilityPicker by remember { mutableStateOf(false) } var uploadedAttachmentNames by remember { mutableStateOf>(emptyList()) } var isUploading by remember { mutableStateOf(false) } diff --git a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListViewModel.kt b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListViewModel.kt index 1f33a75..aeb9ff2 100644 --- a/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/avinal/memos/ui/memos/MemoListViewModel.kt @@ -106,15 +106,10 @@ class MemoListViewModel(private val memoRepository: MemoRepository) : ViewModel( fun toggleTask(memoId: String, lineIndex: Int, checked: Boolean) { viewModelScope.launch { val memo = memoRepository.getMemo(memoId) ?: return@launch - val lines = memo.content.lines().toMutableList() - if (lineIndex !in lines.indices) return@launch - val line = lines[lineIndex] - lines[lineIndex] = if (checked) { - line.replaceFirst("- [ ]", "- [x]") - } else { - line.replaceFirst("- [x]", "- [ ]").replaceFirst("- [X]", "- [ ]") - } - memoRepository.updateMemo(memoId, content = lines.joinToString("\n")) + val tasks = com.avinal.memos.parser.TaskParser.extractTasks(memoId, memo.content) + val task = tasks.find { it.lineIndex == lineIndex } ?: return@launch + val newContent = com.avinal.memos.parser.TaskParser.toggleTaskInContent(memo.content, task) + if (newContent != memo.content) memoRepository.updateMemo(memoId, content = newContent) } }