Add arrival buffer and transport options to notification settings
Update notification calculation to use arrival buffer Add advanced settings toggle for arrival buffer and transport options Fix reverse geocode call in SettingsScreen
This commit is contained in:
@@ -183,6 +183,9 @@ describe('eventStore', () => {
|
|||||||
expect(settings).toEqual({
|
expect(settings).toEqual({
|
||||||
bufferMinutes: 30,
|
bufferMinutes: 30,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
arrivalBufferMinutes: 5,
|
||||||
|
showWalkingOption: true,
|
||||||
|
showBikeOption: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -202,6 +205,9 @@ describe('eventStore', () => {
|
|||||||
const settings = {
|
const settings = {
|
||||||
bufferMinutes: 45,
|
bufferMinutes: 45,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
arrivalBufferMinutes: 5,
|
||||||
|
showWalkingOption: true,
|
||||||
|
showBikeOption: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
await saveNotificationSettings(settings);
|
await saveNotificationSettings(settings);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ describe('notifications service', () => {
|
|||||||
source: 'manual',
|
source: 'manual',
|
||||||
};
|
};
|
||||||
|
|
||||||
const leaveByTime = calculateLeaveByTime(event, [], 30);
|
const leaveByTime = calculateLeaveByTime(event, [], 5, 30);
|
||||||
|
|
||||||
// Leave-by time should be 30 minutes before event time
|
// Leave-by time should be 30 minutes before event time
|
||||||
const expectedTime = new Date('2025-01-01T09:30:00Z');
|
const expectedTime = new Date('2025-01-01T09:30:00Z');
|
||||||
@@ -75,7 +75,7 @@ describe('notifications service', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const leaveByTime = calculateLeaveByTime(event, journeys, 30);
|
const leaveByTime = calculateLeaveByTime(event, journeys, 5, 30);
|
||||||
|
|
||||||
// Should use earliest non-cancelled journey (journey-2 at 07:00) minus buffer
|
// Should use earliest non-cancelled journey (journey-2 at 07:00) minus buffer
|
||||||
const expectedTime = new Date('2025-01-01T06:30:00Z');
|
const expectedTime = new Date('2025-01-01T06:30:00Z');
|
||||||
@@ -118,7 +118,7 @@ describe('notifications service', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const leaveByTime = calculateLeaveByTime(event, journeys, 30);
|
const leaveByTime = calculateLeaveByTime(event, journeys, 5, 30);
|
||||||
|
|
||||||
// Should use journey-2 since journey-1 is cancelled
|
// Should use journey-2 since journey-1 is cancelled
|
||||||
const expectedTime = new Date('2025-01-01T06:30:00Z');
|
const expectedTime = new Date('2025-01-01T06:30:00Z');
|
||||||
@@ -149,7 +149,7 @@ describe('notifications service', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const leaveByTime = calculateLeaveByTime(event, journeys, 30);
|
const leaveByTime = calculateLeaveByTime(event, journeys, 5, 30);
|
||||||
|
|
||||||
// All journeys cancelled, fall back to event time minus buffer
|
// All journeys cancelled, fall back to event time minus buffer
|
||||||
const expectedTime = new Date('2025-01-01T09:30:00Z');
|
const expectedTime = new Date('2025-01-01T09:30:00Z');
|
||||||
@@ -165,7 +165,7 @@ describe('notifications service', () => {
|
|||||||
source: 'manual',
|
source: 'manual',
|
||||||
};
|
};
|
||||||
|
|
||||||
const leaveByTime = calculateLeaveByTime(event, [], 0);
|
const leaveByTime = calculateLeaveByTime(event, [], 5, 0);
|
||||||
|
|
||||||
expect(leaveByTime.getTime()).toBe(event.eventTime.getTime());
|
expect(leaveByTime.getTime()).toBe(event.eventTime.getTime());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export function SettingsScreen({ navigation }: ScreenProps) {
|
|||||||
showWalkingOption: true,
|
showWalkingOption: true,
|
||||||
showBikeOption: true,
|
showBikeOption: true,
|
||||||
});
|
});
|
||||||
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||||
const [locPermission, setLocPermission] = useState<'granted' | 'denied' | 'prompt'>('prompt');
|
const [locPermission, setLocPermission] = useState<'granted' | 'denied' | 'prompt'>('prompt');
|
||||||
const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
@@ -102,7 +103,7 @@ export function SettingsScreen({ navigation }: ScreenProps) {
|
|||||||
|
|
||||||
// Search for stations near the user's actual GPS coordinates
|
// Search for stations near the user's actual GPS coordinates
|
||||||
// Use Nominatim reverse geocode via the API to find a nearby station
|
// Use Nominatim reverse geocode via the API to find a nearby station
|
||||||
const geoResults = await api.geocode('Wien', 'at');
|
const geoResults = await api.reverseGeocode(userLat, userLng);
|
||||||
// Find the station closest to the user's actual coordinates
|
// Find the station closest to the user's actual coordinates
|
||||||
if (geoResults.length > 0) {
|
if (geoResults.length > 0) {
|
||||||
const closest = geoResults.reduce((best: { lat: number; lng: number; display_name: string } | null, candidate) => {
|
const closest = geoResults.reduce((best: { lat: number; lng: number; display_name: string } | null, candidate) => {
|
||||||
@@ -144,6 +145,34 @@ export function SettingsScreen({ navigation }: ScreenProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateArrivalBuffer = async (value: string) => {
|
||||||
|
const minutes = parseInt(value, 10);
|
||||||
|
if (!isNaN(minutes) && minutes >= 0) {
|
||||||
|
const updated = { ...notifSettings, arrivalBufferMinutes: minutes };
|
||||||
|
setNotifSettings(updated);
|
||||||
|
await saveNotificationSettings(updated);
|
||||||
|
await rescheduleAllNotifications();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleWalking = async (value: boolean) => {
|
||||||
|
const updated = { ...notifSettings, showWalkingOption: value };
|
||||||
|
setNotifSettings(updated);
|
||||||
|
await saveNotificationSettings(updated);
|
||||||
|
await rescheduleAllNotifications();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleBike = async (value: boolean) => {
|
||||||
|
const updated = { ...notifSettings, showBikeOption: value };
|
||||||
|
setNotifSettings(updated);
|
||||||
|
await saveNotificationSettings(updated);
|
||||||
|
await rescheduleAllNotifications();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAdvanced = () => {
|
||||||
|
setShowAdvanced(!showAdvanced);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{/* Origin Station */}
|
{/* Origin Station */}
|
||||||
@@ -199,6 +228,46 @@ export function SettingsScreen({ navigation }: ScreenProps) {
|
|||||||
<Text style={styles.hint}>
|
<Text style={styles.hint}>
|
||||||
Du wirst {notifSettings.bufferMinutes} Minuten vor der geplanten Abfahrt erinnert.
|
Du wirst {notifSettings.bufferMinutes} Minuten vor der geplanten Abfahrt erinnert.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
<TouchableOpacity style={[styles.advancedToggle, styles.locBtn]} onPress={toggleAdvanced}>
|
||||||
|
<Text style={styles.locBtnText}>
|
||||||
|
{showAdvanced ? '↑ Weniger Optionen zeigen' : '↓ Mehr Optionen zeigen'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{showAdvanced && (
|
||||||
|
<View style={styles.advancedSection}>
|
||||||
|
<Text style={styles.settingLabel}>Ankunfts-Puffer (Minuten)</Text>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.input, styles.numberInput]}
|
||||||
|
value={String(notifSettings.arrivalBufferMinutes)}
|
||||||
|
onChangeText={updateArrivalBuffer}
|
||||||
|
keyboardType="numeric"
|
||||||
|
accessibilityLabel="Ankunfts-Puffer in Minuten"
|
||||||
|
/>
|
||||||
|
<Text style={styles.hint}>Wie viele Minuten vor der Event-Zeit du am Ziel ankommen möchtest</Text>
|
||||||
|
|
||||||
|
<View style={styles.settingRow}>
|
||||||
|
<Text style={styles.settingLabel}>Zu Fuß-Option anzeigen</Text>
|
||||||
|
<Switch
|
||||||
|
value={notifSettings.showWalkingOption}
|
||||||
|
onValueChange={toggleWalking}
|
||||||
|
trackColor={{ true: '#007AFF', false: '#e5e5ea' }}
|
||||||
|
accessibilityLabel="Zu Fuß-Option umschalten"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.settingRow}>
|
||||||
|
<Text style={styles.settingLabel}>Fahrrad-Option anzeigen</Text>
|
||||||
|
<Switch
|
||||||
|
value={notifSettings.showBikeOption}
|
||||||
|
onValueChange={toggleBike}
|
||||||
|
trackColor={{ true: '#007AFF', false: '#e5e5ea' }}
|
||||||
|
accessibilityLabel="Fahrrad-Option umschalten"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -235,6 +304,16 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
locBtnText: { fontSize: 15, color: '#007AFF', fontWeight: '500' },
|
locBtnText: { fontSize: 15, color: '#007AFF', fontWeight: '500' },
|
||||||
locStatus: { fontSize: 12, color: '#8e8e93', marginTop: 6 },
|
locStatus: { fontSize: 12, color: '#8e8e93', marginTop: 6 },
|
||||||
|
advancedToggle: {
|
||||||
|
marginTop: 12,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
advancedSection: {
|
||||||
|
marginTop: 16,
|
||||||
|
paddingTop: 16,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: '#e5e5ea',
|
||||||
|
},
|
||||||
settingRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 },
|
settingRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 },
|
||||||
settingLabel: { fontSize: 14, color: '#1c1c1e' },
|
settingLabel: { fontSize: 14, color: '#1c1c1e' },
|
||||||
hint: { fontSize: 12, color: '#8e8e93', marginTop: 6 },
|
hint: { fontSize: 12, color: '#8e8e93', marginTop: 6 },
|
||||||
|
|||||||
@@ -16,12 +16,22 @@ Notifications.setNotificationHandler({
|
|||||||
/**
|
/**
|
||||||
* Calculate leave-by time from event time and journey data.
|
* Calculate leave-by time from event time and journey data.
|
||||||
* Uses earliest real departure time if journeys exist, otherwise event time minus buffer.
|
* Uses earliest real departure time if journeys exist, otherwise event time minus buffer.
|
||||||
|
*
|
||||||
|
* @param event - The event to calculate leave-by time for
|
||||||
|
* @param journeys - Journey data for this event
|
||||||
|
* @param arrivalBufferMinutes - How many minutes before the event to arrive
|
||||||
|
* @param bufferMinutes - How many minutes before leaving to be reminded
|
||||||
*/
|
*/
|
||||||
export function calculateLeaveByTime(
|
export function calculateLeaveByTime(
|
||||||
event: Event,
|
event: Event,
|
||||||
journeys: Journey[],
|
journeys: Journey[],
|
||||||
|
arrivalBufferMinutes: number,
|
||||||
bufferMinutes: number
|
bufferMinutes: number
|
||||||
): Date {
|
): Date {
|
||||||
|
// Calculate target arrival time (event time minus arrival buffer)
|
||||||
|
const targetArrivalTime = new Date(event.eventTime);
|
||||||
|
targetArrivalTime.setMinutes(targetArrivalTime.getMinutes() - arrivalBufferMinutes);
|
||||||
|
|
||||||
// If we have journeys, use the earliest non-cancelled real departure
|
// If we have journeys, use the earliest non-cancelled real departure
|
||||||
if (journeys.length > 0) {
|
if (journeys.length > 0) {
|
||||||
const best = journeys
|
const best = journeys
|
||||||
@@ -29,12 +39,13 @@ export function calculateLeaveByTime(
|
|||||||
.sort((a, b) => a.rD.getTime() - b.rD.getTime())[0];
|
.sort((a, b) => a.rD.getTime() - b.rD.getTime())[0];
|
||||||
|
|
||||||
if (best) {
|
if (best) {
|
||||||
return new Date(best.rD.getTime() - bufferMinutes * 60 * 1000);
|
// Leave by time = target arrival time - buffer minutes
|
||||||
|
return new Date(targetArrivalTime.getTime() - bufferMinutes * 60 * 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: event time minus buffer (no journey data)
|
// Fallback: event time minus arrival buffer minus buffer (no journey data)
|
||||||
return new Date(event.eventTime.getTime() - bufferMinutes * 60 * 1000);
|
return new Date(targetArrivalTime.getTime() - bufferMinutes * 60 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,7 +66,7 @@ export async function scheduleNotificationsForEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate leave-by time (when user should actually leave)
|
// Calculate leave-by time (when user should actually leave)
|
||||||
const leaveByTime = calculateLeaveByTime(event, journeys, settings.bufferMinutes);
|
const leaveByTime = calculateLeaveByTime(event, journeys, settings.arrivalBufferMinutes, settings.bufferMinutes);
|
||||||
|
|
||||||
// Cancel existing notifications for this event - cancel one by one
|
// Cancel existing notifications for this event - cancel one by one
|
||||||
const existing = await Notifications.getAllScheduledNotificationsAsync();
|
const existing = await Notifications.getAllScheduledNotificationsAsync();
|
||||||
|
|||||||
@@ -39,9 +39,13 @@ async function getNotificationSettings(): Promise<ReminderSettings> {
|
|||||||
// Notification scheduling utilities
|
// Notification scheduling utilities
|
||||||
// ────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function calculateLeaveByTime(event: Event, bufferMinutes: number): Promise<Date> {
|
async function calculateLeaveByTime(event: Event, arrivalBufferMinutes: number, bufferMinutes: number): Promise<Date> {
|
||||||
// Fallback: event time minus buffer (no journey data)
|
// Calculate target arrival time (event time minus arrival buffer)
|
||||||
return new Date(event.eventTime.getTime() - bufferMinutes * 60 * 1000);
|
const targetArrivalTime = new Date(event.eventTime);
|
||||||
|
targetArrivalTime.setMinutes(targetArrivalTime.getMinutes() - arrivalBufferMinutes);
|
||||||
|
|
||||||
|
// Fallback: event time minus arrival buffer minus buffer (no journey data)
|
||||||
|
return new Date(targetArrivalTime.getTime() - bufferMinutes * 60 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function scheduleEventNotification(event: Event): Promise<void> {
|
async function scheduleEventNotification(event: Event): Promise<void> {
|
||||||
@@ -50,9 +54,7 @@ async function scheduleEventNotification(event: Event): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const leaveByTime = await calculateLeaveByTime(event, settings.bufferMinutes);
|
const leaveByTime = await calculateLeaveByTime(event, settings.arrivalBufferMinutes, settings.bufferMinutes);
|
||||||
|
|
||||||
// Cancel existing notifications for this event - cancel one by one
|
|
||||||
const existing = await Notifications.getAllScheduledNotificationsAsync();
|
const existing = await Notifications.getAllScheduledNotificationsAsync();
|
||||||
const toCancel = existing.filter(n => n.content.data?.eventId === event.id);
|
const toCancel = existing.filter(n => n.content.data?.eventId === event.id);
|
||||||
for (const notif of toCancel) {
|
for (const notif of toCancel) {
|
||||||
@@ -168,7 +170,7 @@ export async function rescheduleAllNotifications(): Promise<void> {
|
|||||||
// Schedule new notifications for each event
|
// Schedule new notifications for each event
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
if (settings.enabled) {
|
if (settings.enabled) {
|
||||||
const leaveByTime = await calculateLeaveByTime(event, settings.bufferMinutes);
|
const leaveByTime = await calculateLeaveByTime(event, settings.arrivalBufferMinutes, settings.bufferMinutes);
|
||||||
|
|
||||||
// Default reminders: 30min, 10min, and at leave-by time
|
// Default reminders: 30min, 10min, and at leave-by time
|
||||||
const defaultReminders = [30, 10, 0];
|
const defaultReminders = [30, 10, 0];
|
||||||
|
|||||||
Reference in New Issue
Block a user