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

@@ -1,18 +1,106 @@
import { Text, View } from "react-native";
import BaseBackground from "../../components/BaseBackground";
import BaseButton from "../../components/BaseButton";
import BaseButton, { BaseButtonProps } from "../../components/BaseButton";
import { useThemeStore } from "../../stores/ThemeStore";
import { AuthService } from "../../services/AuthService";
import { router } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
import { SimpleHeader } from "../../components/Header";
import { THEMES } from "../../Themes";
import CustomTextInput from "../../components/CustomTextInput";
import { useEffect, useState } from "react";
import { CaldavConfigService } from "../../services/CaldavConfigService";
const handleLogout = async () => {
await AuthService.logout();
router.replace("/login");
};
const SettingsButton = (props: BaseButtonProps) => {
return (
<BaseButton
onPress={props.onPress}
solid={props.solid}
className={"w-11/12"}
>
{props.children}
</BaseButton>
);
};
type CaldavTextInputProps = {
title: string;
value: string;
onValueChange: (text: string) => void;
};
const CaldavTextInput = ({ title, value, onValueChange }: CaldavTextInputProps) => {
return (
<View className="flex flex-row items-center py-1">
<Text className="ml-4 w-24">{title}:</Text>
<CustomTextInput className="flex-1 mr-4" text={value} onValueChange={onValueChange} />
</View>
);
};
const CaldavSettings = () => {
const { theme } = useThemeStore();
const [serverUrl, setServerUrl] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
useEffect(() => {
const loadConfig = async () => {
try {
const config = await CaldavConfigService.getConfig();
setServerUrl(config.serverUrl);
setUsername(config.username);
setPassword(config.password);
} catch {
// No config saved yet
}
};
loadConfig();
}, []);
const saveConfig = async () => {
await CaldavConfigService.saveConfig(serverUrl, username, password);
};
const sync = async () => {
await CaldavConfigService.sync();
};
return (
<>
<View>
<Text
className="text-center text-2xl"
style={{ color: theme.textPrimary }}
>
Caldav Config
</Text>
</View>
<View>
<View className="pb-1">
<CaldavTextInput title="url" value={serverUrl} onValueChange={setServerUrl} />
<CaldavTextInput title="username" value={username} onValueChange={setUsername} />
<CaldavTextInput title="password" value={password} onValueChange={setPassword} />
</View>
<View className="flex flex-row">
<BaseButton className="mx-4 w-1/5" solid={true} onPress={saveConfig}>
Save
</BaseButton>
<BaseButton className="w-1/5" solid={true} onPress={sync}>
Sync
</BaseButton>
</View>
</View>
</>
);
};
const Settings = () => {
const { theme, setTheme } = useThemeStore();
@@ -20,10 +108,10 @@ const Settings = () => {
<BaseBackground>
<SimpleHeader text="Settings" />
<View className="flex items-center mt-4">
<BaseButton onPress={handleLogout} solid={true}>
<SettingsButton onPress={handleLogout} solid={true}>
<Ionicons name="log-out-outline" size={24} color={theme.primeFg} />{" "}
Logout
</BaseButton>
</SettingsButton>
<View>
<Text
className="text-center text-2xl"
@@ -32,23 +120,24 @@ const Settings = () => {
Select Theme
</Text>
</View>
<BaseButton
<SettingsButton
solid={theme == THEMES.defaultLight}
onPress={() => {
setTheme("defaultLight");
}}
>
Default Light
</BaseButton>
<BaseButton
</SettingsButton>
<SettingsButton
solid={theme == THEMES.defaultDark}
onPress={() => {
setTheme("defaultDark");
}}
>
Default Dark
</BaseButton>
</SettingsButton>
</View>
<CaldavSettings />
</BaseBackground>
);
};