Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4ee8077
feat(ui): enhance priority visualization and refine home screen layout
ohmzi May 25, 2026
1883db1
Fix iOS task sheet date picker taps
ohmzi May 25, 2026
813f654
style: update color palette and refine list accent colors across plat…
ohmzi May 25, 2026
1d39e53
feat(ux): improve drag-and-drop validation for task rescheduling
ohmzi May 25, 2026
c0813a4
feat(todos): add list deletion and refine timeline spacing
ohmzi May 25, 2026
05f534e
feat(list): implement list deletion and persistence of list associati…
ohmzi May 25, 2026
9da51aa
feat(ux): enhance task completion and restoration animations
ohmzi May 25, 2026
f6d20ce
refactor(calendar): enhance calendar card styling with custom shadow …
ohmzi May 25, 2026
b8c40d5
feat(ux): enhance task completion and restoration animations
ohmzi May 25, 2026
d58ff30
feat(ios): overhaul Todo list settings UI and icon/color selection
ohmzi May 25, 2026
d2b084f
Refactor calendar mode transitions and layout animations in both iOS …
ohmzi May 25, 2026
04cb245
refactor(calendar): simplify calendar mode transition logic and anima…
ohmzi May 25, 2026
97690ff
Refactor calendar mode transition animations in `CalendarScreen`.
ohmzi May 25, 2026
c102628
Refactor UI component logic and optimize performance across Android (…
ohmzi May 26, 2026
d9b1176
ios: refactor SwipeActions to use UIPanGestureRecognizer for smoother…
ohmzi May 26, 2026
16ea835
feat(ux): implement cooldown for offline notifications on iOS and And…
ohmzi May 26, 2026
c8e1d10
feat(ux): implement cooldown for offline notifications on iOS and And…
ohmzi May 26, 2026
aa7be41
fix(calendar): improve drag-and-drop interaction and expand todo visi…
ohmzi May 26, 2026
ddd5f27
refactor(calendar): disable placement animation in CalendarScreen
ohmzi May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions android-compose/app/src/main/java/com/ohmz/tday/compose/TdayApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,12 @@ fun TdayApp(
listName = listName,
onBack = { navController.popBackStack() },
onTaskDeleted = ::showTaskDeletedToast,
onListDeleted = {
navController.navigate(AppRoute.Home.route) {
popUpTo(AppRoute.Home.route) { inclusive = false }
launchSingleTop = true
}
},
)
}

Expand Down Expand Up @@ -816,6 +822,7 @@ private fun TodosRoute(
mode: TodoListMode,
onBack: () -> Unit,
onTaskDeleted: () -> Unit,
onListDeleted: () -> Unit = {},
highlightTodoId: String? = null,
listId: String? = null,
listName: String? = null,
Expand Down Expand Up @@ -855,6 +862,12 @@ private fun TodosRoute(
iconKey = iconKey,
)
},
onDeleteList = { targetListId ->
viewModel.deleteList(
listId = targetListId,
onOptimisticDelete = onListDeleted,
)
},
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ data class CachedCompletedRecord(
val completedAtEpochMs: Long = 0L,
val rrule: String? = null,
val instanceDateEpochMs: Long? = null,
val listId: String? = null,
val listName: String? = null,
val listColor: String? = null,
)
Expand Down Expand Up @@ -79,6 +80,7 @@ data class PendingMutationRecord(
enum class MutationKind {
CREATE_LIST,
UPDATE_LIST,
DELETE_LIST,
CREATE_TODO,
UPDATE_TODO,
DELETE_TODO,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ internal fun completedToCache(item: CompletedItem): CachedCompletedRecord {
completedAtEpochMs = item.completedAt?.toEpochMilli() ?: 0L,
rrule = item.rrule,
instanceDateEpochMs = item.instanceDate?.toEpochMilli(),
listId = item.listId,
listName = item.listName,
listColor = item.listColor,
)
Expand All @@ -132,6 +133,7 @@ internal fun completedFromCache(cache: CachedCompletedRecord): CompletedItem {
},
rrule = cache.rrule,
instanceDate = cache.instanceDateEpochMs?.let(Instant::ofEpochMilli),
listId = cache.listId,
listName = cache.listName,
listColor = cache.listColor,
)
Expand Down Expand Up @@ -171,6 +173,7 @@ internal fun mapCompletedDto(dto: CompletedTodoDto): CompletedItem {
completedAt = parseOptionalInstant(dto.completedAt),
rrule = dto.rrule,
instanceDate = parseOptionalInstant(dto.instanceDate),
listId = dto.listID,
listName = dto.listName,
listColor = dto.listColor,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ class CompletedRepository @Inject constructor(
completedAtEpochMs = completed.completedAtEpochMs.takeIf { it > 0L }
?: timestampMs,
rrule = payload.rrule,
listId = normalizedListId,
listName = listMeta?.name,
listColor = listMeta?.color,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,11 @@ val MIGRATION_2_3 = object : Migration(2, 3) {
)
}
}

val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"ALTER TABLE `cached_completed` ADD COLUMN `listId` TEXT",
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ object DatabaseModule {
TdayDatabase::class.java,
"tday_offline_cache.db",
)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
.allowMainThreadQueries()
.build()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ data class CachedCompletedEntity(
val completedAtEpochMs: Long,
val rrule: String?,
val instanceDateEpochMs: Long?,
val listId: String?,
val listName: String?,
val listColor: String?,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ fun CachedCompletedRecord.toEntity() = CachedCompletedEntity(
completedAtEpochMs = completedAtEpochMs,
rrule = rrule,
instanceDateEpochMs = instanceDateEpochMs,
listId = listId,
listName = listName,
listColor = listColor,
)
Expand All @@ -80,6 +81,7 @@ fun CachedCompletedEntity.toRecord() = CachedCompletedRecord(
completedAtEpochMs = completedAtEpochMs,
rrule = rrule,
instanceDateEpochMs = instanceDateEpochMs,
listId = listId,
listName = listName,
listColor = listColor,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import androidx.room.RoomDatabase
PendingMutationEntity::class,
SyncMetadataEntity::class,
],
version = 3,
version = 4,
exportSchema = false,
)
abstract class TdayDatabase : RoomDatabase() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.ohmz.tday.compose.core.data.isLikelyUnrecoverableMutationError
import com.ohmz.tday.compose.core.data.requireApiBody
import com.ohmz.tday.compose.core.data.sync.SyncManager
import com.ohmz.tday.compose.core.model.CreateListRequest
import com.ohmz.tday.compose.core.model.DeleteListRequest
import com.ohmz.tday.compose.core.model.ListSummary
import com.ohmz.tday.compose.core.model.UpdateListRequest
import com.ohmz.tday.compose.core.model.capitalizeFirstListLetter
Expand Down Expand Up @@ -240,6 +241,87 @@ class ListRepository @Inject constructor(
}
}

suspend fun deleteList(
listId: String,
onOptimisticDelete: () -> Unit = {},
) {
val normalizedListId = listId.trim()
if (normalizedListId.isBlank()) return

val timestampMs = System.currentTimeMillis()
val mutationId = UUID.randomUUID().toString()
val pendingMutation = PendingMutationRecord(
mutationId = mutationId,
kind = MutationKind.DELETE_LIST,
targetId = normalizedListId,
timestampEpochMs = timestampMs,
)
val isLocalOnly = normalizedListId.startsWith(LOCAL_LIST_PREFIX)

cacheManager.updateOfflineState { state ->
val deletedTodoIds = state.todos
.filter { it.listId == normalizedListId }
.map { it.canonicalId }
.toSet()

val prunedMutations = state.pendingMutations.filterNot { mutation ->
mutation.targetId == normalizedListId ||
mutation.listId == normalizedListId ||
deletedTodoIds.contains(mutation.targetId)
}

state.copy(
lists = state.lists.filterNot { it.id == normalizedListId },
todos = state.todos.filterNot { it.listId == normalizedListId },
completedItems = state.completedItems.filterNot { completed ->
completed.listId == normalizedListId ||
completed.originalTodoId?.let(deletedTodoIds::contains) == true
},
pendingMutations = if (isLocalOnly) {
prunedMutations
} else {
prunedMutations + pendingMutation
},
)
}

onOptimisticDelete()

if (isLocalOnly) {
syncManager.syncCachedData(force = true, replayPendingMutations = true)
return
}

val immediateError = runCatching {
requireApiBody(
api.deleteListByBody(DeleteListRequest(id = normalizedListId)),
"Could not delete list",
)
}.exceptionOrNull()

if (immediateError != null && isLikelyUnrecoverableMutationError(
immediateError,
pendingMutation
)
) {
throw immediateError
}

if (immediateError == null) {
cacheManager.updateOfflineState { state ->
state.copy(
pendingMutations = state.pendingMutations.filterNot { it.mutationId == mutationId },
)
}
Log.d(LOG_TAG, "deleteList success listId=$normalizedListId")
} else {
Log.w(
LOG_TAG,
"deleteList deferred listId=$normalizedListId reason=${immediateError.message}"
)
}
}

private fun buildListsForState(state: OfflineSyncState): List<ListSummary> {
val todoCountsByList = state.todos
.asSequence()
Expand All @@ -263,6 +345,9 @@ class ListRepository @Inject constructor(
todos = state.todos.map {
if (it.listId == localListId) it.copy(listId = serverListId) else it
},
completedItems = state.completedItems.map {
if (it.listId == localListId) it.copy(listId = serverListId) else it
},
pendingMutations = state.pendingMutations.map {
it.copy(
targetId = if (it.targetId == localListId) serverListId else it.targetId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.ohmz.tday.compose.core.data.requireApiBody
import com.ohmz.tday.compose.core.model.CompletedItem
import com.ohmz.tday.compose.core.model.CreateListRequest
import com.ohmz.tday.compose.core.model.CreateTodoRequest
import com.ohmz.tday.compose.core.model.DeleteListRequest
import com.ohmz.tday.compose.core.model.DeleteTodoRequest
import com.ohmz.tday.compose.core.model.ListSummary
import com.ohmz.tday.compose.core.model.TodoCompleteRequest
Expand Down Expand Up @@ -274,6 +275,16 @@ class SyncManager @Inject constructor(
true
}

MutationKind.DELETE_LIST -> {
val targetId = resolvedTargetId ?: return@runCatching false
if (targetId.startsWith(LOCAL_LIST_PREFIX)) return@runCatching true
requireApiBody(
api.deleteListByBody(DeleteListRequest(id = targetId)),
"Could not delete list",
)
true
}

MutationKind.CREATE_TODO -> {
val localTodoId = mutation.targetId ?: return@runCatching false
if (!localTodoId.startsWith(LOCAL_TODO_PREFIX)) return@runCatching true
Expand Down Expand Up @@ -536,16 +547,29 @@ class SyncManager @Inject constructor(
localState: OfflineSyncState,
remote: RemoteSnapshot,
): OfflineSyncState {
val remoteTodos = remote.todos.map(::todoToCache)
val pendingDeletedListIds = localState.pendingMutations
.filter { it.kind == MutationKind.DELETE_LIST }
.mapNotNull { it.targetId }
.toSet()
val remoteTodos = remote.todos
.filterNot { it.listId != null && pendingDeletedListIds.contains(it.listId) }
.map(::todoToCache)
val remoteLists = remote.lists.map(::listToCache)
val remoteCompleted = remote.completedItems.map(::completedToCache).toMutableList()
val remoteCompleted = remote.completedItems
.filterNot { it.listId != null && pendingDeletedListIds.contains(it.listId) }
.map(::completedToCache)
.toMutableList()

val pendingTodoCanonicalIds = localState.pendingMutations
.filter { it.kind.affectsTodo() }
.mapNotNull { it.targetId }
.toSet()
val pendingListIds = localState.pendingMutations
.filter { it.kind == MutationKind.CREATE_LIST || it.kind == MutationKind.UPDATE_LIST }
.filter {
it.kind == MutationKind.CREATE_LIST ||
it.kind == MutationKind.UPDATE_LIST ||
it.kind == MutationKind.DELETE_LIST
}
.mapNotNull { it.targetId }
.toSet()
val pendingDeleteAllCanonicals = localState.pendingMutations
Expand Down Expand Up @@ -621,6 +645,10 @@ class SyncManager @Inject constructor(
val localList = localListById[listId]
val remoteList = remoteListById[listId]

if (remoteList != null && pendingDeletedListIds.contains(remoteList.id)) {
return@forEach
}

if (remoteList == null && localList != null) {
val hasPendingLocalMutation = pendingListIds.contains(localList.id)
val isUnsyncedLocalList = localList.id.startsWith(LOCAL_LIST_PREFIX)
Expand Down Expand Up @@ -686,7 +714,11 @@ class SyncManager @Inject constructor(
.mapNotNull { it.targetId }
.toSet()
val pendingListIds = existingPending
.filter { it.kind == MutationKind.CREATE_LIST || it.kind == MutationKind.UPDATE_LIST }
.filter {
it.kind == MutationKind.CREATE_LIST ||
it.kind == MutationKind.UPDATE_LIST ||
it.kind == MutationKind.DELETE_LIST
}
.mapNotNull { it.targetId }
.toSet()
val pendingLocalListCreates = existingPending
Expand Down Expand Up @@ -871,6 +903,9 @@ class SyncManager @Inject constructor(
todos = state.todos.map {
if (it.listId == localListId) it.copy(listId = serverListId) else it
},
completedItems = state.completedItems.map {
if (it.listId == localListId) it.copy(listId = serverListId) else it
},
pendingMutations = state.pendingMutations.map {
it.copy(
targetId = if (it.targetId == localListId) serverListId else it.targetId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,7 @@ class TodoRepository @Inject constructor(
completedAtEpochMs = timestampMs,
rrule = todo.rrule,
instanceDateEpochMs = todo.instanceDateEpochMillis,
listId = todo.listId,
listName = listMeta?.name,
listColor = listMeta?.color,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ data class CompletedItem(
val completedAt: Instant? = null,
val rrule: String?,
val instanceDate: Instant?,
val listId: String? = null,
val listName: String? = null,
val listColor: String? = null,
)
Expand Down
Loading
Loading