1
0
mirror of https://github.com/avinal/nikki.git synced 2026-07-03 21:40:09 +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 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 } }
}
@@ -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 ->
@@ -141,7 +141,13 @@ class MemoRepository(
suspend fun reactToMemo(memoId: String, emoji: String): ApiResult<Unit> {
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")
@@ -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 {
@@ -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)
}
}
@@ -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<List<String>>(emptyList()) }
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) {
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)
}
}