diff --git a/android/app/build.gradle b/android/app/build.gradle
index 2832749..347ae66 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -7,8 +7,8 @@ android {
applicationId "com.donetick.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 10
- versionName "1.0.1"
+ versionCode 13
+ versionName "1.0.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj
index 26d52b8..35da000 100644
--- a/ios/App/App.xcodeproj/project.pbxproj
+++ b/ios/App/App.xcodeproj/project.pbxproj
@@ -355,16 +355,13 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 10;
+ CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_TEAM = 6UJJ78R3BS;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
- MARKETING_VERSION = 1.0.1;
- OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
- PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app;
+ MARKETING_VERSION = 1.0.4;
PRODUCT_NAME = "$(TARGET_NAME)";
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@@ -377,12 +374,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 10;
+ CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_TEAM = 6UJJ78R3BS;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
- MARKETING_VERSION = 1.0.1;
+ MARKETING_VERSION = 1.0.4;
PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
diff --git a/src/App.jsx b/src/App.jsx
index bbae0a9..4d6f1de 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -10,6 +10,8 @@ import { ImpersonateUserProvider } from './contexts/ImpersonateUserContext'
import { AuthenticationProvider } from './service/AuthenticationService'
import { useNotification } from './service/NotificationProvider'
import { apiManager } from './utils/TokenManager'
+
+import { getSafeBottomPadding } from './utils/SafeAreaUtils'
import NetworkBanner from './views/components/NetworkBanner'
const add = className => {
@@ -87,21 +89,21 @@ const AppContent = () => {
}, [needRefresh, showNotification, updateServiceWorker, setNeedRefresh])
return (
- <>
+
)
}
function App() {
const navigate = useNavigate()
startApiManager(navigate)
- startOpenReplay()
+ // startOpenReplay()
const { mode, systemMode } = useColorScheme()
@@ -131,7 +133,7 @@ function App() {
}, [])
return (
-
+
diff --git a/src/CapacitorListener.js b/src/CapacitorListener.js
index 7090da7..cbb8071 100644
--- a/src/CapacitorListener.js
+++ b/src/CapacitorListener.js
@@ -23,8 +23,8 @@ const localNotificationListenerRegistration = () => {
const registerTokenIfNeeded = async (token, deviceInfo, deviceId, platform) => {
try {
- const stored = await Preferences.get({ key: 'deviceRegistration' })
- const lastReg = stored.value ? JSON.parse(stored.value) : null
+ // const stored = await Preferences.get({ key: 'deviceRegistration' })
+ // const lastReg = stored.value ? JSON.parse(stored.value) : null
const current = {
token: token.value,
@@ -34,53 +34,86 @@ const registerTokenIfNeeded = async (token, deviceInfo, deviceId, platform) => {
registeredAt: Date.now(),
}
- const shouldRegister =
- !lastReg ||
- lastReg.token !== current.token ||
- lastReg.appVersion !== current.appVersion ||
- Date.now() - lastReg.registeredAt > 7 * 24 * 60 * 60 * 1000
+ // const shouldRegister =
+ // !lastReg ||
+ // lastReg.token !== current.token ||
+ // lastReg.appVersion !== current.appVersion ||
+ // Date.now() - lastReg.registeredAt > 7 * 24 * 60 * 60 * 1000
- if (shouldRegister) {
- console.log('Registering device token:', {
- reason: !lastReg
- ? 'first_time'
- : lastReg.token !== current.token
- ? 'token_changed'
- : lastReg.appVersion !== current.appVersion
- ? 'app_updated'
- : 'periodic_refresh',
- })
+ // console.log('Registering device token:', {
+ // reason: !lastReg
+ // ? 'first_time'
+ // : lastReg.token !== current.token
+ // ? 'token_changed'
+ // : lastReg.appVersion !== current.appVersion
+ // ? 'app_updated'
+ // : 'periodic_refresh',
+ // })
- const result = await RegisterDeviceToken(
- token.value,
- deviceId.identifier,
- platform,
- deviceInfo.appVersion,
- deviceInfo.model,
- )
-
- if (result && !result.error) {
- await Preferences.set({
- key: 'deviceRegistration',
- value: JSON.stringify(current),
- })
- console.log('Device token registered successfully')
- }
- } else {
- console.log('Device token already registered, skipping')
- }
- } catch (error) {
- console.error(
- 'Error in token registration check, registering anyway:',
- error,
- )
- await RegisterDeviceToken(
+ const result = await RegisterDeviceToken(
token.value,
deviceId.identifier,
platform,
deviceInfo.appVersion,
deviceInfo.model,
)
+
+ if (result && result.ok) {
+ await Preferences.set({
+ key: 'deviceRegistration',
+ value: JSON.stringify(current),
+ })
+ console.log('Device token registered successfully')
+
+ // Emit event to notify UI components of successful registration
+ window.dispatchEvent(new CustomEvent('deviceTokenRegistered'))
+ } else if (result) {
+ // Handle registration errors
+ console.error('Device registration failed:', result.status)
+
+ // Emit event with error details for UI to handle
+ window.dispatchEvent(
+ new CustomEvent('deviceTokenRegistrationFailed', {
+ detail: {
+ status: result.status,
+ error: await result.text().catch(() => 'Unknown error'),
+ },
+ }),
+ )
+ }
+ } catch (error) {
+ console.error(
+ 'Error in token registration check, registering anyway:',
+ error,
+ )
+ const fallbackResult = await RegisterDeviceToken(
+ token.value,
+ deviceId.identifier,
+ platform,
+ deviceInfo.appVersion,
+ deviceInfo.model,
+ )
+
+ if (fallbackResult && fallbackResult.ok) {
+ // Emit event to notify UI components of successful registration
+ window.dispatchEvent(new CustomEvent('deviceTokenRegistered'))
+ } else if (fallbackResult) {
+ // Handle registration errors
+ console.error(
+ 'Fallback device registration failed:',
+ fallbackResult.status,
+ )
+
+ // Emit event with error details for UI to handle
+ window.dispatchEvent(
+ new CustomEvent('deviceTokenRegistrationFailed', {
+ detail: {
+ status: fallbackResult.status,
+ error: await fallbackResult.text().catch(() => 'Unknown error'),
+ },
+ }),
+ )
+ }
}
}
diff --git a/src/contexts/Contexts.jsx b/src/contexts/Contexts.jsx
index 1bef341..542795a 100644
--- a/src/contexts/Contexts.jsx
+++ b/src/contexts/Contexts.jsx
@@ -6,7 +6,7 @@ import SSEProvider from './SSEContext'
import ThemeContext from './ThemeContext'
import WebSocketProvider from './WebSocketContext'
-const Contexts = () => {
+const Contexts = ({ children }) => {
const contexts = [
AlertsProvider,
ThemeContext,
@@ -19,7 +19,7 @@ const Contexts = () => {
return contexts.reduceRight((acc, Context) => {
return {acc}
- }, {})
+ }, children)
}
export default Contexts
diff --git a/src/contexts/QueryContext.jsx b/src/contexts/QueryContext.jsx
index 8de0668..3d775a9 100644
--- a/src/contexts/QueryContext.jsx
+++ b/src/contexts/QueryContext.jsx
@@ -4,10 +4,10 @@ const QueryContext = ({ children }) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
- staleTime: 60000, // 60 seconds
- gcTime: 300000, // 5 minutes
+ staleTime: 300000, // 5 minutes
+ gcTime: 600000, // 10 minutes
refetchOnWindowFocus: false,
- retry: 0,
+ retry: 1,
},
},
})
diff --git a/src/queries/ChoreQueries.jsx b/src/queries/ChoreQueries.jsx
index 68ad1b3..4ba2443 100644
--- a/src/queries/ChoreQueries.jsx
+++ b/src/queries/ChoreQueries.jsx
@@ -3,12 +3,21 @@ import { useState } from 'react'
import { networkManager } from '../hooks/NetworkManager'
import { FEATURES, isFeatureEnabled } from '../utils/FeatureToggle'
import {
+ ApproveChore,
+ ArchiveChore,
CreateChore,
+ DeleteChoreHistory,
GetChoreByID,
GetChoreDetailById,
+ GetChoreHistory,
GetChoresHistory,
GetChoresNew,
+ MarkChoreComplete,
+ RejectChore,
SaveChore,
+ SkipChore,
+ UnArchiveChore,
+ UpdateChoreHistory,
} from '../utils/Fetcher'
import { localStore } from '../utils/LocalStore'
@@ -154,6 +163,8 @@ export const useUpdateChore = () => {
onSuccess: (data, variables) => {
// Invalidate the chores query to refresh the data
queryClient.invalidateQueries(['chores'])
+ // Invalidate history for the specific chore
+ queryClient.invalidateQueries(['choreHistory', variables.id])
},
onMutate: async updatedChore => {
if (!networkManager.isOnline && isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
@@ -255,3 +266,123 @@ export const useChore = choreId => {
},
})
}
+
+export const useArchiveChore = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: ArchiveChore,
+ onSuccess: () => {
+ queryClient.invalidateQueries(['chores'])
+ },
+ })
+}
+
+export const useUnArchiveChore = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: UnArchiveChore,
+ onSuccess: () => {
+ queryClient.invalidateQueries(['chores'])
+ },
+ })
+}
+
+export const useChoreHistory = choreId => {
+ return useQuery({
+ queryKey: ['choreHistory', choreId],
+ queryFn: async () => {
+ if (!choreId) {
+ throw new Error('Chore ID is required to fetch history')
+ }
+ const response = await GetChoreHistory(choreId)
+ if (response && response.ok) {
+ return await response.json()
+ }
+ throw new Error('Failed to fetch chore history')
+ },
+ enabled: !!choreId,
+ staleTime: 0, // Always consider data stale
+ cacheTime: 0, // Don't cache the data
+ refetchOnMount: true, // Always refetch when component mounts
+ refetchOnWindowFocus: true, // Refetch when window gains focus
+ })
+}
+
+export const useUpdateChoreHistory = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: ({ choreId, historyId, historyData }) =>
+ UpdateChoreHistory(choreId, historyId, historyData),
+ onSuccess: (data, { choreId }) => {
+ queryClient.invalidateQueries(['choreHistory', choreId])
+ },
+ })
+}
+
+export const useDeleteChoreHistory = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: ({ choreId, historyId }) =>
+ DeleteChoreHistory(choreId, historyId),
+ onSuccess: (data, { choreId }) => {
+ queryClient.invalidateQueries(['choreHistory', choreId])
+ },
+ })
+}
+
+export const useMarkChoreComplete = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: ({ choreId, body, completedDate, performer }) =>
+ MarkChoreComplete(choreId, body, completedDate, performer),
+ onSuccess: (data, { choreId }) => {
+ queryClient.invalidateQueries(['chores'])
+ queryClient.invalidateQueries(['choreHistory', choreId])
+ queryClient.invalidateQueries(['choreDetails', choreId])
+ },
+ })
+}
+
+export const useSkipChore = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: SkipChore,
+ onSuccess: (data, choreId) => {
+ queryClient.invalidateQueries(['chores'])
+ queryClient.invalidateQueries(['choreHistory', choreId])
+ queryClient.invalidateQueries(['choreDetails', choreId])
+ },
+ })
+}
+
+export const useApproveChore = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: ApproveChore,
+ onSuccess: (data, choreId) => {
+ queryClient.invalidateQueries(['chores'])
+ queryClient.invalidateQueries(['choreHistory', choreId])
+ queryClient.invalidateQueries(['choreDetails', choreId])
+ },
+ })
+}
+
+export const useRejectChore = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: RejectChore,
+ onSuccess: (data, choreId) => {
+ queryClient.invalidateQueries(['chores'])
+ queryClient.invalidateQueries(['choreHistory', choreId])
+ queryClient.invalidateQueries(['choreDetails', choreId])
+ },
+ })
+}
diff --git a/src/queries/ResourceQueries.jsx b/src/queries/ResourceQueries.jsx
index bcae542..21a3679 100644
--- a/src/queries/ResourceQueries.jsx
+++ b/src/queries/ResourceQueries.jsx
@@ -3,7 +3,7 @@ import { GetResource } from '../utils/Fetcher'
export const useResource = () => {
const { data, isLoading, error } = useQuery({
- queryKey: [],
+ queryKey: ['resource'],
queryFn: async () => {
const response = await GetResource()
return response
diff --git a/src/queries/TimeQueries.jsx b/src/queries/TimeQueries.jsx
new file mode 100644
index 0000000..009ea99
--- /dev/null
+++ b/src/queries/TimeQueries.jsx
@@ -0,0 +1,107 @@
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
+import {
+ ClearChoreTimer,
+ DeleteTimeSession,
+ GetChoreTimer,
+ PauseChore,
+ ResetChoreTimer,
+ StartChore,
+ UpdateTimeSession,
+} from '../utils/Fetcher'
+
+export const useChoreTimer = choreId => {
+ return useQuery({
+ queryKey: ['choreTimer', choreId],
+ queryFn: async () => {
+ if (!choreId) {
+ throw new Error('Chore ID is required to fetch timer')
+ }
+ const response = await GetChoreTimer(choreId)
+ if (response && response.ok) {
+ return await response.json()
+ }
+ throw new Error('Failed to fetch chore timer')
+ },
+ enabled: !!choreId,
+ })
+}
+
+export const useStartChore = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: StartChore,
+ onSuccess: (data, choreId) => {
+ queryClient.invalidateQueries(['choreTimer', choreId])
+ queryClient.invalidateQueries(['chores'])
+ queryClient.invalidateQueries(['choreHistory', choreId])
+ },
+ })
+}
+
+export const usePauseChore = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: PauseChore,
+ onSuccess: (data, choreId) => {
+ queryClient.invalidateQueries(['choreTimer', choreId])
+ queryClient.invalidateQueries(['chores'])
+ queryClient.invalidateQueries(['choreHistory', choreId])
+ },
+ })
+}
+
+export const useUpdateTimeSession = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: ({ choreId, sessionId, sessionData }) =>
+ UpdateTimeSession(choreId, sessionId, sessionData),
+ onSuccess: (data, { choreId }) => {
+ queryClient.invalidateQueries(['choreTimer', choreId])
+ queryClient.invalidateQueries(['chores'])
+ queryClient.invalidateQueries(['choreHistory', choreId])
+ },
+ })
+}
+
+export const useDeleteTimeSession = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: ({ choreId, sessionId }) =>
+ DeleteTimeSession(choreId, sessionId),
+ onSuccess: (data, { choreId }) => {
+ queryClient.invalidateQueries(['choreTimer', choreId])
+ queryClient.invalidateQueries(['chores'])
+ queryClient.invalidateQueries(['choreHistory', choreId])
+ },
+ })
+}
+
+export const useResetChoreTimer = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: ResetChoreTimer,
+ onSuccess: (data, choreId) => {
+ queryClient.invalidateQueries(['choreTimer', choreId])
+ queryClient.invalidateQueries(['chores'])
+ queryClient.invalidateQueries(['choreHistory', choreId])
+ },
+ })
+}
+
+export const useClearChoreTimer = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: ClearChoreTimer,
+ onSuccess: (data, choreId) => {
+ queryClient.invalidateQueries(['choreTimer', choreId])
+ queryClient.invalidateQueries(['chores'])
+ queryClient.invalidateQueries(['choreHistory', choreId])
+ },
+ })
+}
diff --git a/src/queries/UserQueries.jsx b/src/queries/UserQueries.jsx
index 772f238..a0162a9 100644
--- a/src/queries/UserQueries.jsx
+++ b/src/queries/UserQueries.jsx
@@ -67,7 +67,7 @@ export const useDeviceTokens = () => {
const result = await resp.json()
return result.res || []
},
- staleTime: 5 * 60 * 1000, // 5 minutes
+ staleTime: 0, // Always fetch fresh data
gcTime: 10 * 60 * 1000, // 10 minutes
})
diff --git a/src/utils/FeatureToggle.js b/src/utils/FeatureToggle.js
index adbcc13..bb2f331 100644
--- a/src/utils/FeatureToggle.js
+++ b/src/utils/FeatureToggle.js
@@ -99,17 +99,20 @@ export const isOfficialDonetickInstance = async () => {
*/
export const isOfficialDonetickInstanceSync = () => {
try {
- // Import here to avoid circular dependencies
- const { apiManager } = require('../utils/TokenManager')
-
- const currentApiUrl = apiManager.getApiURL()
-
- // Check if the API URL contains donetick.com
- return currentApiUrl.toLowerCase().includes('donetick.com')
+ // Dynamic import to avoid circular dependencies
+ return import('../utils/TokenManager').then(({ apiManager }) => {
+ const currentApiUrl = apiManager.getApiURL()
+ // Check if the API URL contains donetick.com
+ return currentApiUrl.toLowerCase().includes('donetick.com')
+ }).catch(error => {
+ console.warn('FeatureToggle: Error checking server instance (sync):', error)
+ // Default to false for safety (self-hosted assumption)
+ return false
+ })
} catch (error) {
console.warn('FeatureToggle: Error checking server instance (sync):', error)
// Default to false for safety (self-hosted assumption)
- return true
+ return false
}
}
diff --git a/src/utils/TokenManager.jsx b/src/utils/TokenManager.jsx
index 98d0065..6331198 100644
--- a/src/utils/TokenManager.jsx
+++ b/src/utils/TokenManager.jsx
@@ -2,6 +2,7 @@ import { Preferences } from '@capacitor/preferences'
import Cookies from 'js-cookie'
import murmurhash from 'murmurhash'
import { API_URL } from '../Config'
+import { FEATURES, isFeatureEnabled } from '../utils/FeatureToggle'
import { networkManager } from '../hooks/NetworkManager'
import { RefreshToken } from './Fetcher'
import { localStore } from './LocalStore'
@@ -105,11 +106,14 @@ export async function Fetch(url, options) {
const response = await fetch(fullURL, options)
if (response.ok) {
- const data = await response.clone().json()
- const optionWithoutToken = { ...options }
- delete optionWithoutToken.headers.Authorization
- const optionsHash = murmurhash.v3(JSON.stringify(optionWithoutToken))
- await localStore.saveToCache(fullURL + optionsHash, data)
+ // Only cache data if offline mode is enabled
+ if (isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
+ const data = await response.clone().json()
+ const optionWithoutToken = { ...options }
+ delete optionWithoutToken.headers.Authorization
+ const optionsHash = murmurhash.v3(JSON.stringify(optionWithoutToken))
+ await localStore.saveToCache(fullURL + optionsHash, data)
+ }
networkManager.setOnline()
} else if (response.status === 401) {
// Handle 401 Unauthorized
@@ -124,15 +128,24 @@ export async function Fetch(url, options) {
response.status === 0
) {
networkManager.setOffline()
- return handleOfflineRequest(fullURL, options)
+ // Only handle offline requests if offline mode is enabled
+ if (isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
+ return handleOfflineRequest(fullURL, options)
+ }
+ // If offline mode is disabled, just throw the error
+ throw new Error(`Request failed with status ${response.status}`)
}
// return promise that resolves to response object:
return Promise.resolve(response)
} catch (error) {
networkManager.setOffline()
console.error('Fetch error:', error)
- // throw error
- return handleOfflineRequest(fullURL, options)
+ // Only handle offline requests if offline mode is enabled
+ if (isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
+ return handleOfflineRequest(fullURL, options)
+ }
+ // If offline mode is disabled, just throw the error
+ throw error
}
}
@@ -184,6 +197,11 @@ export const refreshAccessToken = () => {
}
async function handleOfflineRequest(url, options) {
+ // Only handle offline requests if offline mode is enabled
+ if (!isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
+ throw new Error('Network request failed and offline mode is disabled')
+ }
+
// if get request then attempt to fetch from cache otherewise queue it :
if (options.method === 'GET') {
return attemptFetchFromCache(url, options)
@@ -200,6 +218,11 @@ async function handleOfflineRequest(url, options) {
}
}
async function attemptFetchFromCache(url, options) {
+ // Only attempt cache fetch if offline mode is enabled
+ if (!isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
+ throw new Error('Cache access disabled - offline mode is not enabled')
+ }
+
const optionsHash = murmurhash.v3(JSON.stringify(options))
const cachedData = await localStore.getFromCache(url + optionsHash)
networkManager.setOffline()
diff --git a/src/views/ChoreEdit/ChoreView.jsx b/src/views/ChoreEdit/ChoreView.jsx
index 923a2e1..a43e299 100644
--- a/src/views/ChoreEdit/ChoreView.jsx
+++ b/src/views/ChoreEdit/ChoreView.jsx
@@ -49,18 +49,21 @@ import { ChoreStatus, notInCompletionWindow } from '../../utils/Chores.jsx'
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
import {
ApproveChore,
- DeleteTimeSession,
GetChoreDetailById,
- GetChoreTimer,
MarkChoreComplete,
- PauseChore,
RejectChore,
- ResetChoreTimer,
SkipChore,
- StartChore,
UpdateChorePriority,
} from '../../utils/Fetcher'
+import {
+ useChoreTimer,
+ useDeleteTimeSession,
+ usePauseChore,
+ useResetChoreTimer,
+ useStartChore,
+} from '../../queries/TimeQueries'
import Priorities from '../../utils/Priorities'
+import { getSafeBottomPadding } from '../../utils/SafeAreaUtils.js'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import LoadingComponent from '../components/Loading.jsx'
import RichTextEditor from '../components/RichTextEditor.jsx'
@@ -95,6 +98,12 @@ const ChoreView = () => {
const { data: choreData, isLoading: isChoreLoading } =
useChoreDetails(choreId)
+ const startChore = useStartChore()
+ const pauseChore = usePauseChore()
+ const deleteTimeSession = useDeleteTimeSession()
+ const resetChoreTimer = useResetChoreTimer()
+ const { data: choreTimer } = useChoreTimer(choreId)
+
useEffect(() => {
if (!choreData || !choreData.res || !circleMembersData) {
return
@@ -241,30 +250,26 @@ const ChoreView = () => {
})
}
const handleChoreStart = () => {
- StartChore(choreId).then(response => {
- if (response.ok) {
- response.json().then(data => {
- const newChore = {
- ...chore,
- ...data.res,
- }
- setChore(newChore)
- })
- }
+ startChore.mutate(choreId, {
+ onSuccess: data => {
+ const newChore = {
+ ...chore,
+ ...data.res,
+ }
+ setChore(newChore)
+ },
})
}
const handleChorePause = () => {
- PauseChore(choreId).then(response => {
- if (response.ok) {
- response.json().then(data => {
- const newChore = {
- ...chore,
- ...data.res,
- }
- setChore(newChore)
- })
- }
+ pauseChore.mutate(choreId, {
+ onSuccess: data => {
+ const newChore = {
+ ...chore,
+ ...data.res,
+ }
+ setChore(newChore)
+ },
})
}
@@ -278,17 +283,14 @@ const ChoreView = () => {
cancelText: 'Cancel',
onClose: confirmed => {
if (confirmed) {
- ResetChoreTimer(choreId).then(response => {
- if (response.ok) {
- response.json().then(data => {
- const newChore = {
- ...chore,
- ...data.res,
- }
- setChore(newChore)
- queryClient.invalidateQueries(['chores'])
- })
- }
+ resetChoreTimer.mutate(choreId, {
+ onSuccess: data => {
+ const newChore = {
+ ...chore,
+ ...data.res,
+ }
+ setChore(newChore)
+ },
})
}
setTimerActionConfig({})
@@ -306,22 +308,19 @@ const ChoreView = () => {
cancelText: 'Cancel',
onClose: async confirmed => {
if (confirmed) {
- const resp = await GetChoreTimer(choreId)
- if (resp.ok) {
- const data = await resp.json()
- const sessionId = data?.res?.id
- DeleteTimeSession(choreId, sessionId).then(response => {
- if (response.ok) {
- response.json().then(data => {
+ if (choreTimer?.res?.id) {
+ deleteTimeSession.mutate(
+ { choreId, sessionId: choreTimer.res.id },
+ {
+ onSuccess: data => {
const newChore = {
...chore,
...data.res,
}
setChore(newChore)
- queryClient.invalidateQueries(['chores'])
- })
- }
- })
+ },
+ },
+ )
}
}
setTimerActionConfig({})
@@ -712,6 +711,7 @@ const ChoreView = () => {
p: 2,
borderRadius: 'md',
boxShadow: 'sm',
+ paddingBottom: getSafeBottomPadding(2, '8px'),
}}
variant='soft'
>
diff --git a/src/views/Chores/ArchivedTasks.jsx b/src/views/Chores/ArchivedTasks.jsx
index 8931e7b..810d5ba 100644
--- a/src/views/Chores/ArchivedTasks.jsx
+++ b/src/views/Chores/ArchivedTasks.jsx
@@ -25,14 +25,11 @@ import { useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
+import { useUnArchiveChore } from '../../queries/ChoreQueries'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
import { useNotification } from '../../service/NotificationProvider'
import { ChoreSorter } from '../../utils/Chores'
-import {
- DeleteChore,
- GetArchivedChores,
- UnArchiveChore,
-} from '../../utils/Fetcher'
+import { DeleteChore, GetArchivedChores } from '../../utils/Fetcher'
import LoadingComponent from '../components/Loading'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import ChoreCard from './ChoreCard'
@@ -44,6 +41,7 @@ const ArchivedTasks = () => {
useUserProfile()
const { showSuccess, showError } = useNotification()
const { impersonatedUser } = useImpersonateUser()
+ const unArchiveChore = useUnArchiveChore()
const [archivedChores, setArchivedChores] = useState([])
const [filteredChores, setFilteredChores] = useState([])
const [searchTerm, setSearchTerm] = useState('')
@@ -317,10 +315,20 @@ const ArchivedTasks = () => {
for (const chore of selectedData) {
try {
- await UnArchiveChore(chore.id)
- restoredTasks.push(chore)
+ await new Promise((resolve, reject) => {
+ unArchiveChore.mutate(chore.id, {
+ onSuccess: data => {
+ restoredTasks.push(chore)
+ resolve(data)
+ },
+ onError: error => {
+ failedTasks.push(chore)
+ reject(error)
+ },
+ })
+ })
} catch (error) {
- failedTasks.push(chore)
+ // Error already handled in onError callback
}
}
diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx
index 6cfa0b8..4448c4f 100644
--- a/src/views/Chores/ChoreCard.jsx
+++ b/src/views/Chores/ChoreCard.jsx
@@ -32,6 +32,7 @@ import moment from 'moment'
import React from 'react'
import { useNavigate } from 'react-router-dom'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
+import { usePauseChore, useStartChore } from '../../queries/TimeQueries'
import { useUserProfile } from '../../queries/UserQueries.jsx'
import { useNotification } from '../../service/NotificationProvider'
import { notInCompletionWindow } from '../../utils/Chores.jsx'
@@ -42,9 +43,7 @@ import {
DeleteChore,
MarkChoreComplete,
NudgeChore,
- PauseChore,
RejectChore,
- StartChore,
UpdateChoreAssignee,
UpdateDueDate,
} from '../../utils/Fetcher'
@@ -91,6 +90,8 @@ const ChoreCard = ({
const { impersonatedUser } = useImpersonateUser()
const { showError, showNotification } = useNotification()
+ const startChore = useStartChore()
+ const pauseChore = usePauseChore()
// Swipe functionality state
const [swipeTranslateX, setSwipeTranslateX] = React.useState(0)
@@ -109,7 +110,7 @@ const ChoreCard = ({
setIsTouchDevice('ontouchstart' in window || navigator.maxTouchPoints > 0)
}
checkTouchDevice()
-
+
// Check if this is the official donetick.com instance
try {
setIsOfficialInstance(isOfficialDonetickInstanceSync())
@@ -277,7 +278,10 @@ const ChoreCard = ({
const handleNudge = async ({ choreId, message, notifyAllAssignees }) => {
try {
- const response = await NudgeChore(choreId, { message, notifyAllAssignees })
+ const response = await NudgeChore(choreId, {
+ message,
+ notifyAllAssignees,
+ })
if (response.ok) {
const data = await response.json()
showNotification({
@@ -490,30 +494,26 @@ const ChoreCard = ({
// Handlers for start/pause/complete functionality
const handleChorePause = () => {
- PauseChore(chore.id).then(response => {
- if (response.ok) {
- response.json().then(data => {
- const newChore = {
- ...chore,
- status: data.res.status,
- }
- onChoreUpdate(newChore, 'paused')
- })
- }
+ pauseChore.mutate(chore.id, {
+ onSuccess: data => {
+ const newChore = {
+ ...chore,
+ status: data.res.status,
+ }
+ onChoreUpdate(newChore, 'paused')
+ },
})
}
const handleChoreStart = () => {
- StartChore(chore.id).then(response => {
- if (response.ok) {
- response.json().then(data => {
- const newChore = {
- ...chore,
- status: data.res.status,
- }
- onChoreUpdate(newChore, 'started')
- })
- }
+ startChore.mutate(chore.id, {
+ onSuccess: data => {
+ const newChore = {
+ ...chore,
+ status: data.res.status,
+ }
+ onChoreUpdate(newChore, 'started')
+ },
})
}
@@ -836,7 +836,7 @@ const ChoreCard = ({
-{isOfficialInstance && (
+ {isOfficialInstance && (
{
- PauseChore(chore.id).then(response => {
- if (response.ok) {
- response.json().then(data => {
- const newChore = {
- ...chore,
- ...data.res,
- }
- onChoreUpdate(newChore, 'paused')
- })
- }
+ pauseChore.mutate(chore.id, {
+ onSuccess: data => {
+ const newChore = {
+ ...chore,
+ ...data.res,
+ }
+ onChoreUpdate(newChore, 'paused')
+ },
})
}
const handleChoreStart = () => {
- StartChore(chore.id).then(response => {
- if (response.ok) {
- response.json().then(data => {
- const newChore = {
- ...chore,
- ...data.res,
- }
- onChoreUpdate(newChore, 'started')
- })
- }
+ startChore.mutate(chore.id, {
+ onSuccess: data => {
+ const newChore = {
+ ...chore,
+ ...data.res,
+ }
+ onChoreUpdate(newChore, 'started')
+ },
})
}
diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx
index 6bf80f4..1043e52 100644
--- a/src/views/Chores/MyChores.jsx
+++ b/src/views/Chores/MyChores.jsx
@@ -39,10 +39,9 @@ import {
import Fuse from 'fuse.js'
import { useEffect, useRef, useState } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
-import { useChores } from '../../queries/ChoreQueries'
+import { useChores, useArchiveChore } from '../../queries/ChoreQueries'
import { useNotification } from '../../service/NotificationProvider'
import { TASK_COLOR } from '../../utils/Colors'
-import { ArchiveChore } from '../../utils/Fetcher'
import Priorities from '../../utils/Priorities'
import LoadingComponent from '../components/Loading'
import { useLabels } from '../Labels/LabelQueries'
@@ -76,6 +75,7 @@ const MyChores = () => {
const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('md'))
const { showSuccess, showError, showWarning } = useNotification()
const { impersonatedUser } = useImpersonateUser()
+ const archiveChore = useArchiveChore()
const [chores, setChores] = useState([])
const [filteredChores, setFilteredChores] = useState([])
const [searchFilter, setSearchFilter] = useState('All')
@@ -920,13 +920,23 @@ const MyChores = () => {
const failedTasks = []
for (const chore of selectedData) {
try {
- const archivedChore = await ArchiveChore(chore.id)
- archivedTasks.push(archivedChore)
- // Remove from chores and filteredChores
- setChores(chores.filter(c => c.id !== chore.id))
- setFilteredChores(filteredChores.filter(c => c.id !== chore.id))
+ await new Promise((resolve, reject) => {
+ archiveChore.mutate(chore.id, {
+ onSuccess: (data) => {
+ archivedTasks.push(data)
+ // Remove from chores and filteredChores
+ setChores(prev => prev.filter(c => c.id !== chore.id))
+ setFilteredChores(prev => prev.filter(c => c.id !== chore.id))
+ resolve(data)
+ },
+ onError: (error) => {
+ failedTasks.push(chore)
+ reject(error)
+ }
+ })
+ })
} catch (error) {
- failedTasks.push(chore)
+ // Error already handled in onError callback
}
}
if (archivedTasks.length > 0) {
diff --git a/src/views/History/ChoreHistory.jsx b/src/views/History/ChoreHistory.jsx
index 90b7453..037824a 100644
--- a/src/views/History/ChoreHistory.jsx
+++ b/src/views/History/ChoreHistory.jsx
@@ -24,39 +24,41 @@ import { Link, useParams } from 'react-router-dom'
import useConfirmationModal from '../../hooks/useConfirmationModal'
import { ChoreHistoryStatus } from '../../utils/Chores'
import {
- DeleteChoreHistory,
- GetAllCircleMembers,
- GetChoreHistory,
- UpdateChoreHistory,
-} from '../../utils/Fetcher'
+ useChoreHistory,
+ useDeleteChoreHistory,
+ useUpdateChoreHistory,
+} from '../../queries/ChoreQueries'
+import { useCircleMembers } from '../../queries/UserQueries'
import LoadingComponent from '../components/Loading'
import EditHistoryModal from '../Modals/EditHistoryModal'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import HistoryCard from './HistoryCard'
const ChoreHistory = () => {
- const [choreHistory, setChoresHistory] = useState([])
const [userHistory, setUserHistory] = useState([])
- const [performers, setPerformers] = useState([])
const [historyInfo, setHistoryInfo] = useState([])
-
- const [isLoading, setIsLoading] = useState(true) // Add loading state
const { choreId } = useParams()
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
const [editHistory, setEditHistory] = useState({})
const { confirmModalConfig, showConfirmation } = useConfirmationModal()
+ // React Query hooks
+ const { data: choreHistoryData, isLoading } = useChoreHistory(choreId)
+ const { data: circleMembersData } = useCircleMembers()
+ const updateChoreHistory = useUpdateChoreHistory()
+ const deleteChoreHistory = useDeleteChoreHistory()
+
+ const choreHistory = choreHistoryData?.res || []
+ const performers = circleMembersData?.res || []
+
const handleDelete = historyEntry => {
showConfirmation(
`Are you sure you want to delete this history record?`,
'Delete History Record',
() => {
- DeleteChoreHistory(choreId, historyEntry.id).then(() => {
- const newHistory = choreHistory.filter(
- record => record.id !== historyEntry.id,
- )
- setChoresHistory(newHistory)
- updateHistoryInfo(newHistory, userHistory, performers)
+ deleteChoreHistory.mutate({
+ choreId,
+ historyId: historyEntry.id,
})
},
'Delete',
@@ -71,33 +73,16 @@ const ChoreHistory = () => {
}
useEffect(() => {
- setIsLoading(true) // Start loading
-
- Promise.all([
- GetChoreHistory(choreId).then(res => res.json()),
- GetAllCircleMembers(),
- ])
- .then(([historyData, usersData]) => {
- setChoresHistory(historyData.res)
-
- const newUserChoreHistory = {}
- historyData.res.forEach(choreHistory => {
- const userId = choreHistory.completedBy
- newUserChoreHistory[userId] = (newUserChoreHistory[userId] || 0) + 1
- })
- setUserHistory(newUserChoreHistory)
-
- setPerformers(usersData.res)
- updateHistoryInfo(historyData.res, newUserChoreHistory, usersData.res)
+ if (choreHistory.length > 0 && performers.length > 0) {
+ const newUserChoreHistory = {}
+ choreHistory.forEach(historyEntry => {
+ const userId = historyEntry.completedBy
+ newUserChoreHistory[userId] = (newUserChoreHistory[userId] || 0) + 1
})
- .catch(error => {
- console.error('Error fetching data:', error)
- // Handle errors, e.g., show an error message to the user
- })
- .finally(() => {
- setIsLoading(false) // Finish loading
- })
- }, [choreId])
+ setUserHistory(newUserChoreHistory)
+ updateHistoryInfo(choreHistory, newUserChoreHistory, performers)
+ }
+ }, [choreHistory, performers])
const updateHistoryInfo = (histories, userHistories, performers) => {
// average delay for task completaion from due date:
@@ -327,35 +312,39 @@ const ChoreHistory = () => {
setIsEditModalOpen(false)
},
onSave: updated => {
- UpdateChoreHistory(choreId, editHistory.id, {
- performedAt: updated.performedAt,
- dueDate: updated.dueDate,
- notes: updated.notes,
- }).then(res => {
- if (!res.ok) {
- console.error('Failed to update chore history:', res)
- return
- }
-
- const newRecord = res.json().then(data => {
- const newRecord = data.res
- const newHistory = choreHistory.map(record =>
- record.id === newRecord.id ? newRecord : record,
- )
- setChoresHistory(newHistory)
- setEditHistory(newRecord)
- setIsEditModalOpen(false)
- })
- })
+ updateChoreHistory.mutate(
+ {
+ choreId,
+ historyId: editHistory.id,
+ historyData: {
+ performedAt: updated.performedAt,
+ dueDate: updated.dueDate,
+ notes: updated.notes,
+ },
+ },
+ {
+ onSuccess: data => {
+ setEditHistory(data.res)
+ setIsEditModalOpen(false)
+ },
+ onError: error => {
+ console.error('Failed to update chore history:', error)
+ },
+ },
+ )
},
onDelete: () => {
- DeleteChoreHistory(choreId, editHistory.id).then(() => {
- const newHistory = choreHistory.filter(
- record => record.id !== editHistory.id,
- )
- setChoresHistory(newHistory)
- setIsEditModalOpen(false)
- })
+ deleteChoreHistory.mutate(
+ {
+ choreId,
+ historyId: editHistory.id,
+ },
+ {
+ onSuccess: () => {
+ setIsEditModalOpen(false)
+ },
+ },
+ )
},
}}
historyRecord={editHistory}
diff --git a/src/views/History/HistoryCard.jsx b/src/views/History/HistoryCard.jsx
index 642f9d8..e54e212 100644
--- a/src/views/History/HistoryCard.jsx
+++ b/src/views/History/HistoryCard.jsx
@@ -502,30 +502,23 @@ const HistoryCard = ({
>
}
+ variant='solid'
+ color='success'
+ startDecorator={}
>
- {performer?.displayName || 'Unknown'}
+ Done by {performer?.displayName || 'Unknown'}
{historyEntry.completedBy !== historyEntry.assignedTo &&
assignedTo && (
- <>
-
- →
-
- }
- >
- {assignedTo.displayName}
-
- >
+ }
+ >
+ Assigned to {assignedTo.displayName}
+
)}
{historyEntry.notes && (
diff --git a/src/views/Labels/LabelView.jsx b/src/views/Labels/LabelView.jsx
index 2e4fb46..b8b31a6 100644
--- a/src/views/Labels/LabelView.jsx
+++ b/src/views/Labels/LabelView.jsx
@@ -21,7 +21,7 @@ import LABEL_COLORS, {
getTextColorFromBackgroundColor,
} from '../../utils/Colors'
import { DeleteLabel } from '../../utils/Fetcher'
-import { getSafeBottom, getSafeBottomStyles } from '../../utils/SafeAreaUtils'
+import { getSafeBottomStyles } from '../../utils/SafeAreaUtils'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import { useLabels } from './LabelQueries'
@@ -222,7 +222,7 @@ const LabelCard = ({ label, onEditClick, onDeleteClick, currentUserId }) => {
position: 'absolute',
right: 0,
top: 0,
- bottom: getSafeBottom(),
+ bottom: 0,
width: maxSwipeDistance,
display: 'flex',
alignItems: 'center',
diff --git a/src/views/Modals/Inputs/TimerEditModal.jsx b/src/views/Modals/Inputs/TimerEditModal.jsx
index fa27ec8..d6b4b2e 100644
--- a/src/views/Modals/Inputs/TimerEditModal.jsx
+++ b/src/views/Modals/Inputs/TimerEditModal.jsx
@@ -15,10 +15,10 @@ import { useEffect, useState } from 'react'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
import { useNotification } from '../../../service/NotificationProvider'
import {
- DeleteTimeSession,
- GetChoreTimer,
- UpdateTimeSession,
-} from '../../../utils/Fetcher'
+ useChoreTimer,
+ useDeleteTimeSession,
+ useUpdateTimeSession,
+} from '../../../queries/TimeQueries'
import ConfirmationModal from './ConfirmationModal'
const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
@@ -31,13 +31,17 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
const [currentTime, setCurrentTime] = useState(new Date())
const { showError, showSuccess } = useNotification()
- // Fetch timer data when modal opens
+ // Timer hooks
+ const { data: choreTimer, refetch: refetchTimer } = useChoreTimer(choreId)
+ const updateTimeSession = useUpdateTimeSession()
+ const deleteTimeSession = useDeleteTimeSession()
+
+ // Update timerData when choreTimer data changes
useEffect(() => {
- if (isOpen && choreId) {
- fetchTimerData()
+ if (choreTimer?.res) {
+ setTimerData(choreTimer.res)
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isOpen, choreId])
+ }, [choreTimer])
// Real-time update interval for active timers
useEffect(() => {
@@ -53,28 +57,6 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
}
}, [isOpen, timerData])
- const fetchTimerData = async () => {
- setLoading(true)
- try {
- const response = await GetChoreTimer(choreId)
- if (response.ok) {
- const data = await response.json()
- setTimerData(data.res) // data.res is the timer session object
- } else {
- showError({
- title: 'Failed to fetch timer data',
- message: 'Please try again.',
- })
- }
- } catch (error) {
- showError({
- title: 'Error fetching timer data',
- message: error.message,
- })
- } finally {
- setLoading(false)
- }
- }
const formatTime = seconds => {
const hours = Math.floor(seconds / 3600)
@@ -192,21 +174,26 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
pauseLog: editingData.pauseLog,
}
- const response = await UpdateTimeSession(choreId, sessionId, updateData)
- if (response.ok) {
- showSuccess({
- title: 'Session updated',
- message: 'Timer session has been updated successfully.',
- })
- await fetchTimerData()
- cancelEditingSession(sessionId)
- onTimerUpdate?.()
- } else {
- showError({
- title: 'Failed to update session',
- message: 'Please try again.',
- })
- }
+ updateTimeSession.mutate(
+ { choreId, sessionId, sessionData: updateData },
+ {
+ onSuccess: () => {
+ showSuccess({
+ title: 'Session updated',
+ message: 'Timer session has been updated successfully.',
+ })
+ refetchTimer()
+ cancelEditingSession(sessionId)
+ onTimerUpdate?.()
+ },
+ onError: () => {
+ showError({
+ title: 'Failed to update session',
+ message: 'Please try again.',
+ })
+ },
+ },
+ )
} catch (error) {
showError({
title: 'Error updating session',
@@ -219,29 +206,28 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
const deleteSession = async sessionId => {
setLoading(true)
- try {
- const response = await DeleteTimeSession(choreId, sessionId)
- if (response.ok) {
- showSuccess({
- title: 'Session deleted',
- message: 'Timer session has been deleted successfully.',
- })
- await fetchTimerData()
- onTimerUpdate?.()
- } else {
- showError({
- title: 'Failed to delete session',
- message: 'Please try again.',
- })
- }
- } catch (error) {
- showError({
- title: 'Error deleting session',
- message: error.message,
- })
- } finally {
- setLoading(false)
- }
+ deleteTimeSession.mutate(
+ { choreId, sessionId },
+ {
+ onSuccess: () => {
+ showSuccess({
+ title: 'Session deleted',
+ message: 'Timer session has been deleted successfully.',
+ })
+ refetchTimer()
+ onTimerUpdate?.()
+ },
+ onError: error => {
+ showError({
+ title: 'Error deleting session',
+ message: error.message,
+ })
+ },
+ onSettled: () => {
+ setLoading(false)
+ },
+ },
+ )
}
const confirmDeleteSession = sessionId => {
diff --git a/src/views/Settings/NotificationSetting.jsx b/src/views/Settings/NotificationSetting.jsx
index c219260..ffb042a 100644
--- a/src/views/Settings/NotificationSetting.jsx
+++ b/src/views/Settings/NotificationSetting.jsx
@@ -1,4 +1,5 @@
import { Capacitor } from '@capacitor/core'
+import { Device } from '@capacitor/device'
import { LocalNotifications } from '@capacitor/local-notifications'
import { Preferences } from '@capacitor/preferences'
import { Android, Apple } from '@mui/icons-material'
@@ -72,6 +73,9 @@ const NotificationSetting = () => {
const [naggingNotification, setNaggingNotification] = useState(false)
const [pushNotification, setPushNotification] = useState(false)
const [isOfficialInstance, setIsOfficialInstance] = useState(false)
+ const [currentDevice, setCurrentDevice] = useState(null)
+ const [isCurrentDeviceRegistered, setIsCurrentDeviceRegistered] =
+ useState(true)
useEffect(() => {
getNotificationPreferences().then(resp => {
@@ -95,6 +99,28 @@ const NotificationSetting = () => {
console.warn('Error checking instance type:', error)
setIsOfficialInstance(false)
}
+
+ // Get current device info if on native platform
+ if (Capacitor.isNativePlatform()) {
+ const getCurrentDeviceInfo = async () => {
+ try {
+ const deviceInfo = await Device.getInfo()
+ const deviceId = await Device.getId()
+ const platform =
+ Capacitor.getPlatform() === 'android' ? 'android' : 'ios'
+
+ setCurrentDevice({
+ id: deviceId.identifier,
+ platform,
+ model: deviceInfo.model,
+ appVersion: deviceInfo.appVersion,
+ })
+ } catch (error) {
+ console.error('Error getting device info:', error)
+ }
+ }
+ getCurrentDeviceInfo()
+ }
}, [])
const [notificationTarget, setNotificationTarget] = useState(
@@ -107,6 +133,64 @@ const NotificationSetting = () => {
userProfile?.notification_target?.target_id ?? 0,
)
const [error, setError] = useState('')
+
+ // Check if current device is registered whenever deviceTokens or currentDevice changes
+ useEffect(() => {
+ if (currentDevice && deviceTokens && isOfficialInstance) {
+ const isRegistered = deviceTokens.some(
+ device => device.deviceId === currentDevice.id,
+ )
+ setIsCurrentDeviceRegistered(isRegistered)
+ }
+ }, [currentDevice, deviceTokens, isOfficialInstance])
+
+ // Listen for device registration events from CapacitorListener
+ useEffect(() => {
+ const handleDeviceRegistered = () => {
+ refetchDevices()
+ showWarning({
+ title: 'Success',
+ message: 'Device registered successfully for push notifications.',
+ })
+ }
+
+ const handleDeviceRegistrationFailed = event => {
+ const { status, error } = event.detail || {}
+
+ if (status === 409) {
+ showWarning({
+ title: 'Device Limit Reached',
+ message:
+ 'You have reached the maximum limit of 5 registered devices. Please remove a device before registering this one.',
+ })
+ } else {
+ showWarning({
+ title: 'Registration Failed',
+ message:
+ error ||
+ 'Failed to register device automatically. Please try again.',
+ })
+ }
+ }
+
+ // Listen for the custom events that CapacitorListener might emit
+ window.addEventListener('deviceTokenRegistered', handleDeviceRegistered)
+ window.addEventListener(
+ 'deviceTokenRegistrationFailed',
+ handleDeviceRegistrationFailed,
+ )
+
+ return () => {
+ window.removeEventListener(
+ 'deviceTokenRegistered',
+ handleDeviceRegistered,
+ )
+ window.removeEventListener(
+ 'deviceTokenRegistrationFailed',
+ handleDeviceRegistrationFailed,
+ )
+ }
+ }, [refetchDevices, showWarning])
const SaveValidation = () => {
switch (notificationTarget) {
case '1':
@@ -146,6 +230,55 @@ const NotificationSetting = () => {
alert('Notification target updated')
})
}
+
+ const handleRegisterCurrentDevice = async () => {
+ if (!currentDevice) return
+
+ // Check device limit before attempting registration
+ const currentDeviceCount = deviceTokens ? deviceTokens.length : 0
+ if (currentDeviceCount >= 5) {
+ showWarning({
+ title: 'Device Limit Reached',
+ message:
+ 'You have reached the maximum limit of 5 registered devices. Please remove a device before registering this one.',
+ })
+ return
+ }
+
+ try {
+ // First request push notification permission
+ const permStatus = await PushNotifications.requestPermissions()
+
+ if (permStatus.receive !== 'granted') {
+ showWarning({
+ title: 'Permission Required',
+ message:
+ 'Push notification permission is required to register this device.',
+ })
+ return
+ }
+
+ // Ensure push notification listeners are set up before registration
+
+ await registerPushNotifications()
+
+ // Store registration preferences immediately since permission was granted
+ await setPushNotificationPreferences({ granted: true })
+ setPushNotification(true)
+
+ showWarning({
+ title: 'Registration Initiated',
+ message:
+ 'Push notification registration has been initiated. The device will be registered automatically.',
+ })
+ } catch (error) {
+ console.error('Error registering device:', error)
+ showWarning({
+ title: 'Error',
+ message: 'Failed to register device. Please try again.',
+ })
+ }
+ }
return (
@@ -344,13 +477,60 @@ const NotificationSetting = () => {
{isOfficialInstance && (
<>
- Registered Devices
+ Registered Devices ({deviceTokens ? deviceTokens.length : 0}/5)
Devices registered to receive push notifications for your account
+ {/* Show register current device option if not registered */}
+ {Capacitor.isNativePlatform() &&
+ currentDevice &&
+ !isCurrentDeviceRegistered && (
+
+
+
+ {currentDevice.platform === 'ios' ? (
+
+ ) : (
+
+ )}
+
+
+ Current Device:{' '}
+ {currentDevice.platform === 'ios' ? 'iOS' : 'Android'}{' '}
+ {currentDevice.model}
+
+
+ This device is not registered for push notifications
+
+
+
+
+
+
+ )}
+
{deviceTokens && deviceTokens.length > 0 ? (
{deviceTokens.map(device => (
diff --git a/src/views/Things/ThingsView.jsx b/src/views/Things/ThingsView.jsx
index 73ecba5..0e4791f 100644
--- a/src/views/Things/ThingsView.jsx
+++ b/src/views/Things/ThingsView.jsx
@@ -27,6 +27,7 @@ import {
SaveThing,
UpdateThingState,
} from '../../utils/Fetcher'
+import { getSafeBottomStyles } from '../../utils/SafeAreaUtils'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import CreateThingModal from '../Modals/Inputs/CreateThingModal'
import EditThingStateModal from '../Modals/Inputs/EditThingState'
@@ -724,14 +725,11 @@ const ThingsView = () => {
diff --git a/src/views/Timer/TimerDetails.jsx b/src/views/Timer/TimerDetails.jsx
index 9f1a0cd..3b0d6bb 100644
--- a/src/views/Timer/TimerDetails.jsx
+++ b/src/views/Timer/TimerDetails.jsx
@@ -30,11 +30,11 @@ import { useParams } from 'react-router-dom'
import { useCircleMembers } from '../../queries/UserQueries'
import { useNotification } from '../../service/NotificationProvider'
import {
- GetChoreTimer,
- PauseChore,
- StartChore,
- UpdateTimeSession,
-} from '../../utils/Fetcher'
+ useChoreTimer,
+ usePauseChore,
+ useStartChore,
+ useUpdateTimeSession,
+} from '../../queries/TimeQueries'
import { resolvePhotoURL } from '../../utils/Helpers'
import { getSafeBottom } from '../../utils/SafeAreaUtils'
import LoadingComponent from '../components/Loading'
@@ -60,6 +60,12 @@ const TimerDetails = () => {
const { data: circleMembersData, isLoading: isCircleMembersLoading } =
useCircleMembers()
+ // Timer hooks
+ const { data: choreTimer, refetch: refetchTimer } = useChoreTimer(choreId)
+ const startChore = useStartChore()
+ const pauseChore = usePauseChore()
+ const updateTimeSession = useUpdateTimeSession()
+
const members = circleMembersData?.res || []
// Helper function to find member by user ID
@@ -75,13 +81,12 @@ const TimerDetails = () => {
checkTouchDevice()
}, [])
- // Fetch timer data when component mounts
+ // Update timerData when choreTimer data changes
useEffect(() => {
- if (choreId) {
- fetchTimerData()
+ if (choreTimer?.res) {
+ setTimerData(choreTimer.res)
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [choreId])
+ }, [choreTimer])
// Real-time update interval for active timers
useEffect(() => {
@@ -97,28 +102,6 @@ const TimerDetails = () => {
}
}, [timerData])
- const fetchTimerData = async () => {
- setLoading(true)
- try {
- const response = await GetChoreTimer(choreId)
- if (response.ok) {
- const data = await response.json()
- setTimerData(data.res)
- } else {
- showError({
- title: 'Failed to fetch timer data',
- message: 'Please try again.',
- })
- }
- } catch (error) {
- showError({
- title: 'Error fetching timer data',
- message: error.message,
- })
- } finally {
- setLoading(false)
- }
- }
const formatTime = seconds => {
const hours = Math.floor(seconds / 3600)
@@ -236,20 +219,25 @@ const TimerDetails = () => {
pauseLog: editingData.pauseLog,
}
- const response = await UpdateTimeSession(choreId, sessionId, updateData)
- if (response.ok) {
- showSuccess({
- title: 'Session updated',
- message: 'Timer session has been updated successfully.',
- })
- await fetchTimerData()
- cancelEditingSession(sessionId)
- } else {
- showError({
- title: 'Failed to update session',
- message: 'Please try again.',
- })
- }
+ updateTimeSession.mutate(
+ { choreId, sessionId, sessionData: updateData },
+ {
+ onSuccess: () => {
+ showSuccess({
+ title: 'Session updated',
+ message: 'Timer session has been updated successfully.',
+ })
+ refetchTimer()
+ cancelEditingSession(sessionId)
+ },
+ onError: () => {
+ showError({
+ title: 'Failed to update session',
+ message: 'Please try again.',
+ })
+ },
+ },
+ )
} catch (error) {
showError({
title: 'Error updating session',
@@ -261,56 +249,48 @@ const TimerDetails = () => {
}
// Timer control functions
- const handleStartTimer = async () => {
+ const handleStartTimer = () => {
setTimerActionLoading(true)
- try {
- const response = await StartChore(choreId)
- if (response.ok) {
+ startChore.mutate(choreId, {
+ onSuccess: () => {
showSuccess({
title: 'Timer Started',
message: 'Work session has been started successfully.',
})
- await fetchTimerData()
- } else {
+ refetchTimer()
+ },
+ onError: () => {
showError({
title: 'Failed to start timer',
message: 'Please try again.',
})
- }
- } catch (error) {
- showError({
- title: 'Error starting timer',
- message: error.message,
- })
- } finally {
- setTimerActionLoading(false)
- }
+ },
+ onSettled: () => {
+ setTimerActionLoading(false)
+ },
+ })
}
- const handlePauseTimer = async () => {
+ const handlePauseTimer = () => {
setTimerActionLoading(true)
- try {
- const response = await PauseChore(choreId)
- if (response.ok) {
+ pauseChore.mutate(choreId, {
+ onSuccess: () => {
showSuccess({
title: 'Timer Paused',
message: 'Work session has been paused.',
})
- await fetchTimerData()
- } else {
+ refetchTimer()
+ },
+ onError: () => {
showError({
title: 'Failed to pause timer',
message: 'Please try again.',
})
- }
- } catch (error) {
- showError({
- title: 'Error pausing timer',
- message: error.message,
- })
- } finally {
- setTimerActionLoading(false)
- }
+ },
+ onSettled: () => {
+ setTimerActionLoading(false)
+ },
+ })
}
// Determine if timer is currently running
diff --git a/src/views/components/ChoreActionMenu.jsx b/src/views/components/ChoreActionMenu.jsx
index 546fc8a..8794074 100644
--- a/src/views/components/ChoreActionMenu.jsx
+++ b/src/views/components/ChoreActionMenu.jsx
@@ -26,12 +26,11 @@ import { useNavigate } from 'react-router-dom'
import { useNotification } from '../../service/NotificationProvider'
import { isOfficialDonetickInstanceSync } from '../../utils/FeatureToggle'
import {
- ArchiveChore,
DeleteChore,
SkipChore,
- UnArchiveChore,
UpdateDueDate,
} from '../../utils/Fetcher'
+import { useArchiveChore, useUnArchiveChore } from '../../queries/ChoreQueries'
const ChoreActionMenu = ({
chore,
@@ -55,6 +54,8 @@ const ChoreActionMenu = ({
const menuRef = React.useRef(null)
const navigate = useNavigate()
const { showError } = useNotification()
+ const archiveChore = useArchiveChore()
+ const unArchiveChore = useUnArchiveChore()
// Check if this is the official donetick.com instance
useEffect(() => {
@@ -126,22 +127,18 @@ const ChoreActionMenu = ({
const handleArchive = () => {
if (chore.isActive) {
- ArchiveChore(chore.id).then(response => {
- if (response.ok) {
- response.json().then(() => {
- const newChore = { ...chore, isActive: false }
- onChoreUpdate?.(newChore, 'archive')
- })
- }
+ archiveChore.mutate(chore.id, {
+ onSuccess: () => {
+ const newChore = { ...chore, isActive: false }
+ onChoreUpdate?.(newChore, 'archive')
+ },
})
} else {
- UnArchiveChore(chore.id).then(response => {
- if (response.ok) {
- response.json().then(() => {
- const newChore = { ...chore, isActive: true }
- onChoreUpdate?.(newChore, 'unarchive')
- })
- }
+ unArchiveChore.mutate(chore.id, {
+ onSuccess: () => {
+ const newChore = { ...chore, isActive: true }
+ onChoreUpdate?.(newChore, 'unarchive')
+ },
})
}
handleMenuClose()
diff --git a/vite.config.js b/vite.config.js
index 6d6044b..593b81d 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,4 +1,4 @@
-import react from '@vitejs/plugin-react-swc'
+import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
// https://vitejs.dev/config/
@@ -14,10 +14,6 @@ export default defineConfig({
'safari-pinned-tab.svg',
'mstile-150x150.png',
],
- injectManifest: {
- globPatterns: ['**/*.{js,css,html,png,svg}'],
- globIgnores: ['index.html'],
- },
manifest: {
name: 'Donetick: Simplify Tasks & Chores, Together.',
short_name: 'Donetick',