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:
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
35
apps/client/src/components/CustomTextInput.tsx
Normal file
35
apps/client/src/components/CustomTextInput.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user