diff --git a/src/utils/CommandQueue.js b/src/utils/CommandQueue.js
index 44eb303..f9f8794 100644
--- a/src/utils/CommandQueue.js
+++ b/src/utils/CommandQueue.js
@@ -26,6 +26,34 @@ class CommandQueue {
return sanitized
}
+ _clearPendingFlags(chore = {}) {
+ const next = { ...chore }
+ delete next._pending
+ delete next._pendingUpdate
+ return next
+ }
+
+ async _rollbackCancelledCommand(command) {
+ if (!command) return
+
+ if (
+ command.commandType !== CommandType.ARCHIVE_CHORE &&
+ command.commandType !== CommandType.UNARCHIVE_CHORE
+ ) {
+ return
+ }
+
+ const cachedChore = await offlineDB.getChore(command.entityId)
+ if (!cachedChore) return
+
+ const restoredChore = this._clearPendingFlags({
+ ...cachedChore,
+ isActive: command.commandType === CommandType.ARCHIVE_CHORE,
+ })
+
+ await offlineDB.saveChores([restoredChore])
+ }
+
// Enqueue a domain command
async enqueue(type, entityId, payload) {
if (!isOfflineFeatureEnabled()) {
@@ -81,6 +109,11 @@ class CommandQueue {
// Cancel/undo a pending command
async cancel(commandId) {
if (!isOfflineFeatureEnabled()) return
+
+ const allCommands = await offlineDB.getCommands()
+ const command = allCommands.find(c => String(c.id) === String(commandId))
+
+ await this._rollbackCancelledCommand(command)
return offlineDB.removeCommand(commandId)
}
diff --git a/src/views/ChoreEdit/ChoreView.jsx b/src/views/ChoreEdit/ChoreView.jsx
index 6a38d45..6f8de7a 100644
--- a/src/views/ChoreEdit/ChoreView.jsx
+++ b/src/views/ChoreEdit/ChoreView.jsx
@@ -536,6 +536,7 @@ const ChoreView = () => {
try {
const response = await UnArchiveChore(choreId)
if (response.ok) {
+ await offlineDB.saveChores([{ ...chore, isActive: true }])
setChore({ ...chore, isActive: true })
queryClient.invalidateQueries(['chores'])
}
@@ -548,12 +549,16 @@ const ChoreView = () => {
choreId,
{ id: choreId },
)
+ await offlineDB.saveChores([
+ { ...chore, isActive: true, _pending: 'unarchive' },
+ ])
setChore({ ...chore, isActive: true })
queryClient.invalidateQueries({ queryKey: ['pendingCommands'] })
showSuccess({
message: "You're offline — restore will sync when back online",
undoAction: async () => {
await commandQueue.cancel(cmdId)
+ await offlineDB.saveChores([{ ...chore, isActive: false }])
setChore({ ...chore, isActive: false })
queryClient.invalidateQueries({ queryKey: ['pendingCommands'] })
},
diff --git a/src/views/Chores/ArchivedTasks.jsx b/src/views/Chores/ArchivedTasks.jsx
index 27250e6..c292ad3 100644
--- a/src/views/Chores/ArchivedTasks.jsx
+++ b/src/views/Chores/ArchivedTasks.jsx
@@ -1,24 +1,24 @@
import {
- Archive,
- CheckBox,
- CheckBoxOutlineBlank,
- Close,
- Delete,
- SelectAll,
- Unarchive,
- ViewAgenda,
- ViewModule,
+ Archive,
+ CheckBox,
+ CheckBoxOutlineBlank,
+ Close,
+ Delete,
+ SelectAll,
+ Unarchive,
+ ViewAgenda,
+ ViewModule,
} from '@mui/icons-material'
import {
- Box,
- Button,
- Container,
- Divider,
- IconButton,
- Input,
- List,
- Stack,
- Typography,
+ Box,
+ Button,
+ Container,
+ Divider,
+ IconButton,
+ Input,
+ List,
+ Stack,
+ Typography,
} from '@mui/joy'
import { useQueryClient } from '@tanstack/react-query'
import Fuse from 'fuse.js'
@@ -31,6 +31,7 @@ import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
import { useNotification } from '../../service/NotificationProvider'
import { commandQueue, CommandType } from '../../utils/CommandQueue'
import { DeleteChore, GetArchivedChores } from '../../utils/Fetcher'
+import { offlineDB } from '../../utils/OfflineDB'
import LoadingComponent from '../components/Loading'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import ChoreCard from './ChoreCard'
@@ -38,6 +39,48 @@ import ChoreListView from './ChoreListView.jsx'
import CompactChoreCard from './CompactChoreCard'
import MultiSelectHelp from './MultiSelectHelp'
+const sortByUpdatedAtDesc = chores =>
+ (chores || []).sort((a, b) => {
+ const dateA = new Date(a.updatedAt || 0)
+ const dateB = new Date(b.updatedAt || 0)
+ return dateB - dateA
+ })
+
+const applyPendingArchivedState = async chores => {
+ const pending = await commandQueue.getPending()
+
+ const pendingArchiveIds = new Set(
+ pending
+ .filter(cmd => cmd.commandType === CommandType.ARCHIVE_CHORE)
+ .map(cmd => String(cmd.entityId)),
+ )
+ const pendingUnarchiveIds = new Set(
+ pending
+ .filter(cmd => cmd.commandType === CommandType.UNARCHIVE_CHORE)
+ .map(cmd => String(cmd.entityId)),
+ )
+ const pendingDeleteIds = new Set(
+ pending
+ .filter(cmd => cmd.commandType === CommandType.DELETE_CHORE)
+ .map(cmd => String(cmd.entityId)),
+ )
+
+ return (chores || [])
+ .filter(chore => !pendingDeleteIds.has(String(chore.id)))
+ .filter(chore => {
+ const id = String(chore.id)
+ if (pendingUnarchiveIds.has(id)) return false
+ return chore.isActive === false || pendingArchiveIds.has(id)
+ })
+ .map(chore => {
+ const id = String(chore.id)
+ if (pendingArchiveIds.has(id)) {
+ return { ...chore, isActive: false, _pending: 'archive' }
+ }
+ return chore
+ })
+}
+
const ArchivedTasks = () => {
const { data: userProfile, isLoading: isUserProfileLoading } =
useUserProfile()
@@ -71,19 +114,28 @@ const ArchivedTasks = () => {
try {
const response = await GetArchivedChores()
const data = await response.json()
- // Sort by updatedAt (most recent first)
- const sortedChores = data.res.sort((a, b) => {
- const dateA = new Date(a.updatedAt || 0)
- const dateB = new Date(b.updatedAt || 0)
- return dateB - dateA
- })
+ if (data?.res?.length) {
+ await offlineDB.saveChores(data.res)
+ }
+ const archivedWithPending = await applyPendingArchivedState(
+ data?.res || [],
+ )
+ const sortedChores = sortByUpdatedAtDesc(archivedWithPending)
setArchivedChores(sortedChores)
setFilteredChores(sortedChores)
} catch (error) {
- showError({
- title: 'Failed to load archived tasks',
- message: 'Please try again later.',
- })
+ try {
+ const cached = await offlineDB.getChores(true)
+ const archivedWithPending = await applyPendingArchivedState(cached)
+ const sortedChores = sortByUpdatedAtDesc(archivedWithPending)
+ setArchivedChores(sortedChores)
+ setFilteredChores(sortedChores)
+ } catch {
+ showError({
+ title: 'Failed to load archived tasks',
+ message: 'Please try again later.',
+ })
+ }
} finally {
setIsLoading(false)
}
@@ -337,6 +389,9 @@ const ArchivedTasks = () => {
onError: async error => {
if (isNetworkError(error)) {
await commandQueue.enqueue(CommandType.UNARCHIVE_CHORE, chore.id, { id: chore.id })
+ await offlineDB.saveChores([
+ { ...chore, isActive: true, _pending: 'unarchive' },
+ ])
queuedTasks.push(chore)
resolve()
} else {
diff --git a/src/views/Chores/hooks/useChoreActions.js b/src/views/Chores/hooks/useChoreActions.js
index d7b4d10..6bf279e 100644
--- a/src/views/Chores/hooks/useChoreActions.js
+++ b/src/views/Chores/hooks/useChoreActions.js
@@ -481,6 +481,9 @@ export const useChoreActions = ({
chore.id,
{ id: chore.id },
)
+ await offlineDB.saveChores([
+ { ...chore, isActive: false, _pending: 'archive' },
+ ])
setChores(prev => prev.filter(c => c.id !== chore.id))
setFilteredChores(prev =>
prev.filter(c => c.id !== chore.id),
@@ -493,6 +496,9 @@ export const useChoreActions = ({
"You're offline — archive will sync when back online",
undoAction: async () => {
await commandQueue.cancel(cmdId)
+ await offlineDB.saveChores([
+ { ...chore, isActive: true },
+ ])
queryClient.invalidateQueries({
queryKey: ['pendingCommands'],
})
@@ -529,6 +535,9 @@ export const useChoreActions = ({
chore.id,
{ id: chore.id },
)
+ await offlineDB.saveChores([
+ { ...chore, isActive: true, _pending: 'unarchive' },
+ ])
queryClient.invalidateQueries({
queryKey: ['pendingCommands'],
})
@@ -537,6 +546,9 @@ export const useChoreActions = ({
"You're offline — restore will sync when back online",
undoAction: async () => {
await commandQueue.cancel(cmdId)
+ await offlineDB.saveChores([
+ { ...chore, isActive: false },
+ ])
queryClient.invalidateQueries({
queryKey: ['pendingCommands'],
})
diff --git a/src/views/Settings/AdvancedSettings.jsx b/src/views/Settings/AdvancedSettings.jsx
index b3f723d..d97ad46 100644
--- a/src/views/Settings/AdvancedSettings.jsx
+++ b/src/views/Settings/AdvancedSettings.jsx
@@ -73,7 +73,7 @@ const AdvancedSettings = () => {
queryClient.invalidateQueries()
showNotification({
type: 'success',
- message: 'Offline support disabled and local offline data cleared',
+ message: 'Offline mode turned off and local data was cleared',
})
} catch {
setOfflineFeatureEnabled(false)
@@ -83,7 +83,7 @@ const AdvancedSettings = () => {
showNotification({
type: 'warning',
message:
- 'Offline support disabled, but some local cache items may not have been cleared',
+ 'Offline mode was turned off, but some local data may still be stored',
})
} finally {
setOfflineLoading(false)
@@ -93,10 +93,10 @@ const AdvancedSettings = () => {
const showDisableOfflineConfirmation = () => {
setConfirmModalConfig({
isOpen: true,
- title: 'Disable Offline Support',
+ title: 'Turn Off Offline Mode',
message:
- 'Disabling offline support will remove queued offline actions and local cached offline data on this device/browser. Do you want to continue?',
- confirmText: 'Disable & Clear',
+ 'Turning off offline mode will remove unsynced offline changes and saved offline data on this device/browser. Do you want to continue?',
+ confirmText: 'Turn Off & Clear Data',
cancelText: 'Cancel',
color: 'danger',
onClose: isConfirmed => {
@@ -117,7 +117,7 @@ const AdvancedSettings = () => {
queryClient.invalidateQueries()
showNotification({
type: 'success',
- message: 'Offline support enabled for this device/browser',
+ message: 'Offline mode turned on for this device/browser',
})
return
}
@@ -158,8 +158,8 @@ const AdvancedSettings = () => {
- Enable offline queue and local cache for this device/browser.
- Disabling removes pending offline actions and cached offline data.
+ Keep using Donetick when you're offline on this device/browser. Your
+ changes are saved locally and synced when you're back online.
{
overlay
/>
- When disabled, queued changes and offline cache are cleared from
- this device/browser.
+ Turning this off removes unsynced offline changes and saved offline
+ data from this device/browser.