1
0
mirror of https://github.com/avinal/nikki.git synced 2026-07-04 05:50:10 +05:30

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 <avinal.xlvii@gmail.com>
This commit is contained in:
2026-05-21 17:25:57 +05:30
parent 2f60ac3e13
commit 25a5c51b0a
7 changed files with 34 additions and 27 deletions
@@ -39,8 +39,11 @@ class AppDependencies(
val authRepository: AuthRepository by lazy { AuthRepository(apiClient, tokenStore) } val authRepository: AuthRepository by lazy { AuthRepository(apiClient, tokenStore) }
val memoRepository: MemoRepository by lazy { MemoRepository(apiClient, database.memoDao()) } val memoRepository: MemoRepository by lazy { MemoRepository(apiClient, database.memoDao()) }
private var initJob: kotlinx.coroutines.Job? = null
fun initialize() { fun initialize() {
CoroutineScope(Dispatchers.IO).launch { initJob?.cancel()
initJob = CoroutineScope(Dispatchers.IO).launch {
launch { tokenStore.accessToken.collect { cachedToken = it } } launch { tokenStore.accessToken.collect { cachedToken = it } }
launch { tokenStore.serverUrl.collect { cachedServerUrl = it } } launch { tokenStore.serverUrl.collect { cachedServerUrl = it } }
} }
@@ -19,6 +19,11 @@ object HttpClientFactory {
encodeDefaults = true encodeDefaults = true
}) })
} }
install(io.ktor.client.plugins.HttpTimeout) {
requestTimeoutMillis = 30_000
connectTimeoutMillis = 15_000
socketTimeoutMillis = 30_000
}
} }
client.plugin(HttpSend).intercept { request -> client.plugin(HttpSend).intercept { request ->
@@ -141,7 +141,13 @@ class MemoRepository(
suspend fun reactToMemo(memoId: String, emoji: String): ApiResult<Unit> { suspend fun reactToMemo(memoId: String, emoji: String): ApiResult<Unit> {
return when (apiClient.upsertReaction(memoId, emoji)) { return when (apiClient.upsertReaction(memoId, emoji)) {
is ApiResult.Success -> { 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) ApiResult.Success(Unit)
} }
is ApiResult.Error -> ApiResult.Error(0, "Failed to react") is ApiResult.Error -> ApiResult.Error(0, "Failed to react")
@@ -276,7 +276,9 @@ private fun ExplorerPage(
Spacer(Modifier.height(8.dp)) 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) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
dayLabels.forEach { day -> dayLabels.forEach { day ->
Text(day, fontSize = 11.sp, color = subtleColor.copy(alpha = 0.5f), modifier = Modifier.width(28.dp), textAlign = TextAlign.Center) 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)) Spacer(Modifier.height(4.dp))
val firstDayOfWeek = remember(calYear, calMonthIdx) { val firstDayOfWeek = remember(calYear, calMonthIdx, weekStartDay) {
val month = calMonthIdx + 1 val month = calMonthIdx + 1
val y = if (month <= 2) calYear - 1 else calYear val y = if (month <= 2) calYear - 1 else calYear
val m = if (month <= 2) month + 12 else month 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 h = (1 + (13 * (m + 1)) / 5 + y + y / 4 - y / 100 + y / 400) % 7
val mondayBased = ((h + 5) % 7) val adjusted = ((h + 6 - weekStartDay) % 7)
if (mondayBased < 0) mondayBased + 7 else mondayBased if (adjusted < 0) adjusted + 7 else adjusted
} }
val cells = buildList { val cells = buildList {
@@ -37,18 +37,11 @@ class MemoDetailViewModel(
fun toggleTask(lineIndex: Int, checked: Boolean) { fun toggleTask(lineIndex: Int, checked: Boolean) {
val current = memo.value ?: return val current = memo.value ?: return
val lines = current.content.lines().toMutableList() val tasks = com.avinal.memos.parser.TaskParser.extractTasks(memoId, current.content)
if (lineIndex !in lines.indices) return val task = tasks.find { it.lineIndex == lineIndex } ?: return
val line = lines[lineIndex]
lines[lineIndex] = if (checked) {
line.replaceFirst("- [ ]", "- [x]")
} else {
line.replaceFirst("- [x]", "- [ ]").replaceFirst("- [X]", "- [ ]")
}
viewModelScope.launch { 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)
} }
} }
@@ -100,7 +100,10 @@ fun MemoListScreen(
val subtleColor = MaterialTheme.colorScheme.onSurfaceVariant val subtleColor = MaterialTheme.colorScheme.onSurfaceVariant
var composeText by remember { mutableStateOf("") } 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 showVisibilityPicker by remember { mutableStateOf(false) }
var uploadedAttachmentNames by remember { mutableStateOf<List<String>>(emptyList()) } var uploadedAttachmentNames by remember { mutableStateOf<List<String>>(emptyList()) }
var isUploading by remember { mutableStateOf(false) } var isUploading by remember { mutableStateOf(false) }
@@ -106,15 +106,10 @@ class MemoListViewModel(private val memoRepository: MemoRepository) : ViewModel(
fun toggleTask(memoId: String, lineIndex: Int, checked: Boolean) { fun toggleTask(memoId: String, lineIndex: Int, checked: Boolean) {
viewModelScope.launch { viewModelScope.launch {
val memo = memoRepository.getMemo(memoId) ?: return@launch val memo = memoRepository.getMemo(memoId) ?: return@launch
val lines = memo.content.lines().toMutableList() val tasks = com.avinal.memos.parser.TaskParser.extractTasks(memoId, memo.content)
if (lineIndex !in lines.indices) return@launch val task = tasks.find { it.lineIndex == lineIndex } ?: return@launch
val line = lines[lineIndex] val newContent = com.avinal.memos.parser.TaskParser.toggleTaskInContent(memo.content, task)
lines[lineIndex] = if (checked) { if (newContent != memo.content) memoRepository.updateMemo(memoId, content = newContent)
line.replaceFirst("- [ ]", "- [x]")
} else {
line.replaceFirst("- [x]", "- [ ]").replaceFirst("- [X]", "- [ ]")
}
memoRepository.updateMemo(memoId, content = lines.joinToString("\n"))
} }
} }