working push notifiations

This commit is contained in:
2026-08-23 11:36:17 +02:00
parent 9312783793
commit bdc6562116
49 changed files with 1858 additions and 419 deletions
+41 -32
View File
@@ -1,5 +1,5 @@
import React, { useMemo, useRef, useState, useCallback } from 'react';
import { View, Text, StyleSheet, SafeAreaView, ScrollView, TouchableOpacity, Animated, Dimensions, BackHandler, KeyboardAvoidingView } from 'react-native';
import React, { useMemo, useRef, useState, useCallback, useEffect } from 'react';
import { View, Text, StyleSheet, SafeAreaView, ScrollView, TouchableOpacity, Animated, Dimensions, KeyboardAvoidingView } from 'react-native';
import { useRouter, useFocusEffect } from 'expo-router';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { Header } from '@/components/Header';
@@ -8,7 +8,7 @@ import { useSubtasks } from '@/hooks/useSubtasks';
import { useTaskModals } from '@/hooks/useTaskModals';
import { useSettings } from '@/theme';
import { useCategories } from '@/hooks/useDatabase';
import { toggleTaskComplete } from '@/utils/taskActions';
import { confirmAndToggleTaskComplete } from '@/utils/taskActions';
import { QuickAddBar } from '@/components/QuickAddBar';
import { OptionPickerModal } from '@/components/OptionPickerModal';
import Task from '@/models/Task';
@@ -27,6 +27,7 @@ const MONTH_NAMES = [
export default function CalendarScreen() {
const router = useRouter();
const { theme } = useSettings();
const styles = React.useMemo(() => createStyles(theme), [theme]);
const categories = useCategories();
const { modals, openTaskEdit } = useTaskModals();
@@ -37,9 +38,18 @@ export default function CalendarScreen() {
const { map: subtasksByTask, refresh: refreshSubtasks } = useSubtasks();
const visibleMonthRef = useRef(visibleMonth);
visibleMonthRef.current = visibleMonth;
const selectedDateRef = useRef(selectedDate);
selectedDateRef.current = selectedDate;
const translateX = useRef<Animated.Value>(new Animated.Value(0));
const animatingRef = useRef(false);
const gridWidthRef = useRef<number>(WIDTH);
useEffect(() => {
visibleMonthRef.current = visibleMonth;
}, [visibleMonth]);
useEffect(() => {
selectedDateRef.current = selectedDate;
}, [selectedDate]);
const { tasks: selectedDayTasks, refresh: refreshDayTasks } = useTasksByDate(selectedDate);
const monthTasks = useTasksInMonth(visibleMonth);
@@ -47,18 +57,12 @@ export default function CalendarScreen() {
useFocusEffect(
useCallback(() => {
const sub = BackHandler.addEventListener('hardwareBackPress', () => true);
refreshDayTasks();
refreshMonthTasks();
refreshSubtasks();
return () => sub.remove();
}, [refreshDayTasks, refreshMonthTasks, refreshSubtasks])
);
const translateX = useRef(new Animated.Value(0)).current;
const animatingRef = useRef(false);
const gridWidthRef = useRef<number>(WIDTH);
const weeks = useMemo(() => {
const first = startOfMonth(visibleMonth);
const offset = (first.getDay() + 6) % 7; // week starts Monday
@@ -78,16 +82,16 @@ export default function CalendarScreen() {
}
if (!animate) {
setVisibleMonth(next);
translateX.setValue(0);
translateX.current.setValue(0);
return;
}
animatingRef.current = true;
const w = gridWidthRef.current || WIDTH;
const target = dir === 1 ? -w : w;
Animated.timing(translateX, { toValue: target, duration: 220, useNativeDriver: false }).start(() => {
Animated.timing(translateX.current, { toValue: target, duration: 220, useNativeDriver: false }).start(() => {
setVisibleMonth(next);
translateX.setValue(-target);
Animated.timing(translateX, { toValue: 0, duration: 180, useNativeDriver: false }).start(() => {
translateX.current.setValue(-target);
Animated.timing(translateX.current, { toValue: 0, duration: 180, useNativeDriver: false }).start(() => {
animatingRef.current = false;
});
});
@@ -113,21 +117,23 @@ export default function CalendarScreen() {
.activeOffsetX([-16, 16])
.minDistance(6)
.runOnJS(true)
// eslint-disable-next-line react-hooks/refs
.onUpdate((e) => {
if (!animatingRef.current) translateX.setValue(e.translationX);
if (!animatingRef.current) translateX.current.setValue(e.translationX);
})
// eslint-disable-next-line react-hooks/refs
.onEnd((e) => {
if (animatingRef.current) return;
const w = gridWidthRef.current || WIDTH;
const dx = e.translationX;
if (dx <= -w / 4) {
translateX.stopAnimation();
translateX.current.stopAnimation();
transitionTo(1, false);
} else if (dx >= w / 4) {
translateX.stopAnimation();
translateX.current.stopAnimation();
transitionTo(-1, false);
} else {
Animated.spring(translateX, { toValue: 0, useNativeDriver: false }).start();
Animated.spring(translateX.current, { toValue: 0, useNativeDriver: false }).start();
}
}),
[translateX, transitionTo]
@@ -152,10 +158,11 @@ export default function CalendarScreen() {
}, [currentYear]);
const handleToggleComplete = useCallback(async (taskId: string) => {
await toggleTaskComplete(taskId);
refreshDayTasks();
refreshMonthTasks();
refreshSubtasks();
await confirmAndToggleTaskComplete(taskId, () => {
refreshDayTasks();
refreshMonthTasks();
refreshSubtasks();
});
}, [refreshDayTasks, refreshMonthTasks, refreshSubtasks]);
return (
@@ -169,7 +176,8 @@ export default function CalendarScreen() {
onLayout={(e) => {
gridWidthRef.current = e.nativeEvent.layout.width;
}}
style={[styles.calendarArea, { transform: [{ translateX }] }]}
// eslint-disable-next-line react-hooks/refs
style={[styles.calendarArea, { transform: [{ translateX: translateX.current }] }]}
>
<View style={styles.monthRow}>
<TouchableOpacity
@@ -221,7 +229,7 @@ export default function CalendarScreen() {
key={day.toISOString()}
day={day}
label={cellTitle(byDayOf(monthTasks.byDay, day))}
dotColor={cellColor(byDayOf(monthTasks.byDay, day), categories)}
dotColor={cellColor(byDayOf(monthTasks.byDay, day), categories, theme)}
selected={isSameDay(day, selectedDate)}
today={isToday(day)}
inMonth={isSameMonth(day, visibleMonth)}
@@ -349,10 +357,10 @@ function cellTitle(tasks: Task[]): string | null {
return `${tasks[0].title} +${tasks.length - 1}`;
}
function cellColor(tasks: Task[], categories: { id: string; color: string }[]): string {
if (tasks.length === 0) return '#8E8E8E';
function cellColor(tasks: Task[], categories: { id: string; color: string }[], theme: ThemeColors): string {
if (tasks.length === 0) return theme.textMuted;
const cat = categories.find((c) => c.id === tasks[0].categoryId);
return cat ? desaturate(cat.color, 0.3) : '#8E8E8E';
return cat ? desaturate(cat.color, 0.3) : theme.textMuted;
}
interface DayCellProps {
@@ -367,6 +375,7 @@ interface DayCellProps {
}
const DayCell = React.memo(function DayCell({ day, label, dotColor, selected, today, inMonth, theme, onPress }: DayCellProps) {
const styles = React.useMemo(() => createStyles(theme), [theme]);
return (
<TouchableOpacity
style={[styles.dayCell, selected && { backgroundColor: theme.accentSoft, borderColor: theme.accentBorder }]}
@@ -397,7 +406,7 @@ const DayCell = React.memo(function DayCell({ day, label, dotColor, selected, to
);
});
const styles = StyleSheet.create({
const createStyles = (theme: ThemeColors) => StyleSheet.create({
container: {
flex: 1,
},
@@ -485,7 +494,7 @@ const styles = StyleSheet.create({
fontWeight: '600',
},
dayNumberSelected: {
color: '#FFFFFF',
color: theme.text,
},
chip: {
marginTop: 3,
@@ -558,7 +567,7 @@ const styles = StyleSheet.create({
},
eventCompleted: {
textDecorationLine: 'line-through',
color: '#9E9E9E',
color: theme.textMuted,
},
menuButton: {
padding: 4,
@@ -585,6 +594,6 @@ const styles = StyleSheet.create({
},
bulletCompleted: {
textDecorationLine: 'line-through',
color: '#6E6E6E',
color: theme.textMuted,
},
});