feat: add CalDAV synchronization with automatic sync

- Add CaldavService with tsdav/ical.js for CalDAV server communication
- Add CaldavController, CaldavRepository, and caldav routes
- Add client-side CaldavConfigService with sync(), config CRUD
- Add CalDAV settings UI with config load/save in settings screen
- Sync on login, auto-login (AuthGuard), periodic timer (calendar), and sync button
- Push single events to CalDAV on server-side create/update/delete
- Push all events to CalDAV after chat event confirmation
- Refactor ChatService to use EventService instead of direct EventRepository
- Rename CalDav/calDav to Caldav/caldav for consistent naming
- Add Radicale Docker setup for local CalDAV testing
- Update PlantUML diagrams and CLAUDE.md with CalDAV architecture
This commit is contained in:
2026-02-08 19:24:59 +01:00
parent 81221d8b70
commit 325246826a
44 changed files with 7074 additions and 126 deletions

View File

@@ -3,6 +3,7 @@ import { View, ActivityIndicator } from "react-native";
import { Redirect } from "expo-router";
import { useAuthStore } from "../stores";
import { useThemeStore } from "../stores/ThemeStore";
import { CaldavConfigService } from "../services/CaldavConfigService";
type AuthGuardProps = {
children: ReactNode;
@@ -20,7 +21,16 @@ export const AuthGuard = ({ children }: AuthGuardProps) => {
const { isAuthenticated, isLoading, loadStoredUser } = useAuthStore();
useEffect(() => {
loadStoredUser();
const init = async () => {
await loadStoredUser();
if (!useAuthStore.getState().isAuthenticated) return;
try {
await CaldavConfigService.sync();
} catch {
// No CalDAV config or sync failed — not critical
}
};
init();
}, [loadStoredUser]);
if (isLoading) {

View File

@@ -2,17 +2,18 @@ import { Pressable, Text } from "react-native";
import { useThemeStore } from "../stores/ThemeStore";
import { ReactNode } from "react";
type BaseButtonProps = {
export type BaseButtonProps = {
children?: ReactNode;
className?: string;
onPress: () => void;
solid?: boolean;
};
const BaseButton = ({ children, onPress, solid = false }: BaseButtonProps) => {
const BaseButton = ({ className, children, onPress, solid = false }: BaseButtonProps) => {
const { theme } = useThemeStore();
return (
<Pressable
className="w-11/12 rounded-lg p-4 mb-4 border-4"
className={`rounded-lg p-4 mb-4 border-4 ${className}`}
onPress={onPress}
style={{
borderColor: theme.borderPrimary,

View File

@@ -0,0 +1,35 @@
import { TextInput } from "react-native";
import { useThemeStore } from "../stores/ThemeStore";
import { useState } from "react";
export type CustomTextInputProps = {
text?: string;
focused?: boolean;
className?: string;
multiline?: boolean;
onValueChange?: (text: string) => void;
};
const CustomTextInput = (props: CustomTextInputProps) => {
const { theme } = useThemeStore();
const [focused, setFocused] = useState(props.focused ?? false);
return (
<TextInput
className={`border border-solid rounded-2xl px-3 py-2 h-11/12 ${props.className}`}
onChangeText={props.onValueChange}
value={props.text}
multiline={props.multiline}
style={{
backgroundColor: theme.messageBorderBg,
color: theme.textPrimary,
textAlignVertical: "top",
borderColor: focused ? theme.chatBot : theme.borderPrimary,
}}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
/>
);
};
export default CustomTextInput;