From a64627e7d616130725cb1c112f1932521b13f0fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Thu, 1 Jan 2026 15:02:40 +0100 Subject: [PATCH] Reminders. --- .../CalendarSettingsPageViewModel.cs | 50 +++++- .../EventDetailsPageViewModel.cs | 153 +++++++++++++++++- .../Entities/Calendar/Reminder.cs | 6 +- .../Interfaces/ICalendarService.cs | 7 + .../Interfaces/IPreferencesService.cs | 5 + .../Processors/GmailChangeProcessor.cs | 60 +++++++ .../Processors/OutlookChangeProcessor.cs | 23 +++ .../Helpers/CalendarXamlHelpers.cs | 1 + .../Personalization/PreDefinedAppTheme.cs | 2 +- .../Services/PreferencesService.cs | 6 + .../Views/Calendar/CalendarSettingsPage.xaml | 10 ++ .../Views/Calendar/EventDetailsPage.xaml | 54 +++++-- Wino.Mail.WinUI/Wino.Mail.WinUI.csproj | 34 ++++ Wino.Services/CalendarService.cs | 33 ++++ 14 files changed, 420 insertions(+), 24 deletions(-) diff --git a/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs index 13c46202..887249a7 100644 --- a/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarSettingsPageViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging; using Wino.Core.Domain.Interfaces; @@ -35,13 +36,22 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel [ObservableProperty] public partial int WorkingDayEndIndex { get; set; } + + [ObservableProperty] + public partial List ReminderOptions { get; set; } = []; + + [ObservableProperty] + public partial int SelectedDefaultReminderIndex { get; set; } + public IPreferencesService PreferencesService { get; } + private readonly ICalendarService _calendarService; private readonly bool _isLoaded = false; - public CalendarSettingsPageViewModel(IPreferencesService preferencesService) + public CalendarSettingsPageViewModel(IPreferencesService preferencesService, ICalendarService calendarService) { PreferencesService = preferencesService; + _calendarService = calendarService; var currentLanguageLanguageCode = WinoTranslationDictionary.GetLanguageFileNameRelativePath(preferencesService.CurrentLanguage); @@ -62,6 +72,31 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel WorkingDayStartIndex = DayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart)); WorkingDayEndIndex = DayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd)); + // Initialize reminder options + var predefinedMinutes = _calendarService.GetPredefinedReminderMinutes(); + ReminderOptions.Add("None"); + foreach (var minutes in predefinedMinutes) + { + var displayText = minutes switch + { + >= 60 => $"{minutes / 60} Hour{(minutes / 60 > 1 ? "s" : "")}", + _ => $"{minutes} Minute{(minutes > 1 ? "s" : "")}" + }; + ReminderOptions.Add(displayText); + } + + // Set selected index based on current default reminder setting + if (preferencesService.DefaultReminderDurationInSeconds == 0) + { + SelectedDefaultReminderIndex = 0; // None + } + else + { + var minutes = (int)(preferencesService.DefaultReminderDurationInSeconds / 60); + var index = Array.IndexOf(predefinedMinutes, minutes); + SelectedDefaultReminderIndex = index >= 0 ? index + 1 : 0; + } + _isLoaded = true; } @@ -72,6 +107,7 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel partial void OnWorkingHourEndChanged(TimeSpan value) => SaveSettings(); partial void OnWorkingDayStartIndexChanged(int value) => SaveSettings(); partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings(); + partial void OnSelectedDefaultReminderIndexChanged(int value) => SaveSettings(); public void SaveSettings() { @@ -118,6 +154,18 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel PreferencesService.WorkingHourEnd = WorkingHourEnd; PreferencesService.HourHeight = CellHourHeight; + // Save default reminder setting + if (SelectedDefaultReminderIndex == 0) + { + PreferencesService.DefaultReminderDurationInSeconds = 0; // None + } + else + { + var predefinedMinutes = _calendarService.GetPredefinedReminderMinutes(); + var minutes = predefinedMinutes[SelectedDefaultReminderIndex - 1]; + PreferencesService.DefaultReminderDurationInSeconds = minutes * 60; + } + Messenger.Send(new CalendarSettingsUpdatedMessage()); } } diff --git a/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs b/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs index 4230e8ef..068e85b2 100644 --- a/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs +++ b/Wino.Calendar.ViewModels/EventDetailsPageViewModel.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using Wino.Calendar.ViewModels.Data; using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Calendar; @@ -32,10 +35,14 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanViewSeries))] [NotifyPropertyChangedFor(nameof(CanEditSeries))] - private CalendarItemViewModel _currentEvent; - + [NotifyPropertyChangedFor(nameof(IsCurrentUserOrganizer))] + public partial CalendarItemViewModel CurrentEvent { get; set; } [ObservableProperty] - private CalendarItemViewModel _seriesParent; + public partial CalendarItemViewModel SeriesParent { get; set; } + [ObservableProperty] + public partial List Reminders { get; set; } + + public ObservableCollection ReminderOptions { get; } = new ObservableCollection(); /// /// Returns true if the event is part of a recurring series (as a child occurrence). @@ -49,6 +56,12 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel /// public bool CanEditSeries => CurrentEvent?.IsRecurringChild ?? false; + /// + /// Returns true if the current user is the organizer of the event. + /// Used to determine if the user can invite attendees or modify the event. + /// + public bool IsCurrentUserOrganizer => CurrentEvent?.Attendees?.Any(a => a.IsOrganizer) ?? true; + #endregion #region Show As Options @@ -113,6 +126,10 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel { CurrentEvent.Attendees.Add(item); } + + // Load reminders for this calendar item + Reminders = await _calendarService.GetRemindersAsync(currentEventItem.EventTrackingId); + InitializeReminderOptions(); } catch (Exception ex) { @@ -120,17 +137,88 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel } } - public override void OnNavigatedFrom(NavigationMode mode, object parameters) + private void InitializeReminderOptions() { - base.OnNavigatedFrom(mode, parameters); + ReminderOptions.Clear(); - Messenger.Send(new DetailsPageStateChangedMessage(false)); + // Add predefined options from service + var predefinedMinutes = _calendarService.GetPredefinedReminderMinutes(); + var predefinedOptions = predefinedMinutes.Select(m => new ReminderOption(m)).ToList(); + + // Add custom reminders from synced data + if (Reminders != null) + { + foreach (var reminder in Reminders) + { + // Convert seconds to minutes + var minutesDiff = (int)(reminder.DurationInSeconds / 60); + + // Check if this is a custom value not in predefined list + if (!predefinedMinutes.Contains(minutesDiff)) + { + predefinedOptions.Add(new ReminderOption(minutesDiff, isCustom: true)); + } + } + } + + // Sort by minutes descending and add to collection + foreach (var option in predefinedOptions.OrderByDescending(o => o.Minutes)) + { + ReminderOptions.Add(option); + } + + // Set selected state based on current reminders + if (Reminders != null) + { + foreach (var reminder in Reminders) + { + // Convert seconds to minutes + var minutesDiff = (int)(reminder.DurationInSeconds / 60); + + var matchingOption = ReminderOptions.FirstOrDefault(o => o.Minutes == minutesDiff); + matchingOption?.IsSelected = true; + } + } } + [RelayCommand] private async Task SaveAsync() { - // TODO: Implement saving + if (CurrentEvent == null) return; + + try + { + // Get selected reminder options + var selectedOptions = ReminderOptions.Where(o => o.IsSelected).ToList(); + + // Create separate Reminder entities for each selected option + var newReminders = new List(); + + foreach (var option in selectedOptions) + { + var durationInSeconds = option.Minutes * 60; // Convert minutes to seconds + + newReminders.Add(new Reminder + { + Id = Guid.NewGuid(), + CalendarItemId = CurrentEvent.Id, + DurationInSeconds = durationInSeconds, + ReminderType = CalendarItemReminderType.Popup + }); + } + + // Save reminders to database + await _calendarService.SaveRemindersAsync(CurrentEvent.CalendarItem.EventTrackingId, newReminders); + Reminders = newReminders; + + _navigationService.GoBack(); + // TODO: Implement saving other event details + } + catch (Exception ex) + { + Debug.WriteLine($"Error saving event: {ex.Message}"); + } } [RelayCommand] @@ -183,4 +271,55 @@ public partial class EventDetailsPageViewModel : CalendarBaseViewModel // TODO: Implement response } + + [RelayCommand] + private async Task ViewSeriesAsync() + { + if (CurrentEvent == null || !CurrentEvent.IsRecurringChild) return; + + try + { + // Get the master event from the recurring series + var masterEventId = CurrentEvent.CalendarItem.RecurringCalendarItemId.Value; + var masterEvent = await _calendarService.GetCalendarItemAsync(masterEventId); + + if (masterEvent == null) return; + + // Load the master event without navigation + var target = new CalendarItemTarget(masterEvent, CalendarEventTargetType.Series); + await LoadCalendarItemTargetAsync(target); + } + catch (Exception ex) + { + Debug.WriteLine($"Error loading series: {ex.Message}"); + } + } +} + +public partial class ReminderOption : ObservableObject +{ + public int Minutes { get; } + public bool IsCustom { get; } + + [ObservableProperty] + public partial bool IsSelected { get; set; } + + public string DisplayText + { + get + { + if (Minutes >= 60) + { + var hours = Minutes / 60; + return hours == 1 ? "1 Hour" : $"{hours} Hours"; + } + return Minutes == 1 ? "1 Minute" : $"{Minutes} Minutes"; + } + } + + public ReminderOption(int minutes, bool isCustom = false) + { + Minutes = minutes; + IsCustom = isCustom; + } } diff --git a/Wino.Core.Domain/Entities/Calendar/Reminder.cs b/Wino.Core.Domain/Entities/Calendar/Reminder.cs index 3dc64b81..389c4cef 100644 --- a/Wino.Core.Domain/Entities/Calendar/Reminder.cs +++ b/Wino.Core.Domain/Entities/Calendar/Reminder.cs @@ -10,6 +10,10 @@ public class Reminder public Guid Id { get; set; } public Guid CalendarItemId { get; set; } - public DateTimeOffset ReminderTime { get; set; } + /// + /// Duration in seconds before the event start time when the reminder should trigger. + /// For example, 900 seconds = 15 minutes before event. + /// + public long DurationInSeconds { get; set; } public CalendarItemReminderType ReminderType { get; set; } } diff --git a/Wino.Core.Domain/Interfaces/ICalendarService.cs b/Wino.Core.Domain/Interfaces/ICalendarService.cs index b0e7d0c1..bd958f73 100644 --- a/Wino.Core.Domain/Interfaces/ICalendarService.cs +++ b/Wino.Core.Domain/Interfaces/ICalendarService.cs @@ -47,4 +47,11 @@ public interface ICalendarService Task> GetAttendeesAsync(Guid calendarEventTrackingId); Task> ManageEventAttendeesAsync(Guid calendarItemId, List allAttendees); Task UpdateCalendarItemAsync(CalendarItem calendarItem, List attendees); + Task> GetRemindersAsync(Guid calendarItemId); + Task SaveRemindersAsync(Guid calendarItemId, List reminders); + + /// + /// Gets predefined reminder options in minutes (1 Hour, 30 Min, 15 Min, 5 Min, 1 Min). + /// + int[] GetPredefinedReminderMinutes(); } diff --git a/Wino.Core.Domain/Interfaces/IPreferencesService.cs b/Wino.Core.Domain/Interfaces/IPreferencesService.cs index eeb07bda..9266cd22 100644 --- a/Wino.Core.Domain/Interfaces/IPreferencesService.cs +++ b/Wino.Core.Domain/Interfaces/IPreferencesService.cs @@ -216,6 +216,11 @@ public interface IPreferencesService : INotifyPropertyChanged DayOfWeek WorkingDayEnd { get; set; } double HourHeight { get; set; } + /// + /// Setting: Default reminder duration in seconds for new calendar events. + /// Set to 0 to disable default reminders. + /// + long DefaultReminderDurationInSeconds { get; set; } CalendarSettings GetCurrentCalendarSettings(); diff --git a/Wino.Core/Integration/Processors/GmailChangeProcessor.cs b/Wino.Core/Integration/Processors/GmailChangeProcessor.cs index 8b3e694a..9329752c 100644 --- a/Wino.Core/Integration/Processors/GmailChangeProcessor.cs +++ b/Wino.Core/Integration/Processors/GmailChangeProcessor.cs @@ -223,7 +223,37 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso } } + // Prepare reminders list from Gmail event + List reminders = null; + if (calendarEvent.Reminders?.Overrides != null && calendarEvent.Reminders.Overrides.Count > 0) + { + reminders = new List(); + foreach (var reminderOverride in calendarEvent.Reminders.Overrides) + { + if (reminderOverride.Minutes.HasValue) + { + var durationInSeconds = reminderOverride.Minutes.Value * 60; // Convert minutes to seconds + var reminderType = reminderOverride.Method switch + { + "email" => CalendarItemReminderType.Email, + _ => CalendarItemReminderType.Popup + }; + + reminders.Add(new Reminder + { + Id = Guid.NewGuid(), + CalendarItemId = calendarItem.Id, + DurationInSeconds = durationInSeconds, + ReminderType = reminderType + }); + } + } + } + await CalendarService.CreateNewCalendarItemAsync(calendarItem, attendees); + + // Save reminders separately + await CalendarService.SaveRemindersAsync(calendarItem.Id, reminders).ConfigureAwait(false); } else { @@ -255,6 +285,36 @@ public class GmailChangeProcessor : DefaultChangeProcessor, IGmailChangeProcesso // Update the event properties. } + + // Prepare reminders list from Gmail event for update + List reminders = null; + if (calendarEvent.Reminders?.Overrides != null && calendarEvent.Reminders.Overrides.Count > 0) + { + reminders = new List(); + foreach (var reminderOverride in calendarEvent.Reminders.Overrides) + { + if (reminderOverride.Minutes.HasValue) + { + var durationInSeconds = reminderOverride.Minutes.Value * 60; // Convert minutes to seconds + var reminderType = reminderOverride.Method switch + { + "email" => CalendarItemReminderType.Email, + _ => CalendarItemReminderType.Popup + }; + + reminders.Add(new Reminder + { + Id = Guid.NewGuid(), + CalendarItemId = existingCalendarItem.Id, + DurationInSeconds = durationInSeconds, + ReminderType = reminderType + }); + } + } + } + + // Save reminders + await CalendarService.SaveRemindersAsync(existingCalendarItem.Id, reminders).ConfigureAwait(false); } // Upsert the event. diff --git a/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs b/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs index 640c6cfa..176ac7dc 100644 --- a/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs +++ b/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs @@ -10,6 +10,7 @@ using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Extensions; using Wino.Services; +using Reminder = Wino.Core.Domain.Entities.Calendar.Reminder; namespace Wino.Core.Integration.Processors; @@ -175,6 +176,25 @@ public class OutlookChangeProcessor(IDatabaseService databaseService, attendees = calendarEvent.Attendees.Select(a => a.CreateAttendee(savingItemId, organizerEmail)).ToList(); } + // Prepare reminders list from Outlook event + List reminders = null; + if (calendarEvent.IsReminderOn.GetValueOrDefault() && calendarEvent.ReminderMinutesBeforeStart.HasValue) + { + var reminderMinutes = calendarEvent.ReminderMinutesBeforeStart.Value; + var reminderDurationInSeconds = reminderMinutes * 60; // Convert minutes to seconds + + reminders = new List + { + new Reminder + { + Id = Guid.NewGuid(), + CalendarItemId = savingItemId, + DurationInSeconds = reminderDurationInSeconds, + ReminderType = CalendarItemReminderType.Popup + } + }; + } + // Use CalendarService to create or update the event if (isNewItem) { @@ -186,5 +206,8 @@ public class OutlookChangeProcessor(IDatabaseService databaseService, // Existing item - use UpdateCalendarItemAsync await CalendarService.UpdateCalendarItemAsync(savingItem, attendees).ConfigureAwait(false); } + + // Save reminders separately + await CalendarService.SaveRemindersAsync(savingItemId, reminders).ConfigureAwait(false); } } diff --git a/Wino.Mail.WinUI/Helpers/CalendarXamlHelpers.cs b/Wino.Mail.WinUI/Helpers/CalendarXamlHelpers.cs index dffe475d..23f06614 100644 --- a/Wino.Mail.WinUI/Helpers/CalendarXamlHelpers.cs +++ b/Wino.Mail.WinUI/Helpers/CalendarXamlHelpers.cs @@ -42,6 +42,7 @@ public static class CalendarXamlHelpers public static string GetRecurrenceString(CalendarItemViewModel calendarItemViewModel) { + // TODO: This is incorrect. if (calendarItemViewModel == null || string.IsNullOrEmpty(calendarItemViewModel.CalendarItem.Recurrence)) return string.Empty; // Parse recurrence rules diff --git a/Wino.Mail.WinUI/Models/Personalization/PreDefinedAppTheme.cs b/Wino.Mail.WinUI/Models/Personalization/PreDefinedAppTheme.cs index e53a236f..69e73204 100644 --- a/Wino.Mail.WinUI/Models/Personalization/PreDefinedAppTheme.cs +++ b/Wino.Mail.WinUI/Models/Personalization/PreDefinedAppTheme.cs @@ -27,7 +27,7 @@ public class PreDefinedAppTheme : AppThemeBase public override async Task GetThemeResourceDictionaryContentAsync() { - var xamlDictionaryFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri($"ms-appx:///Wino.Mail.WinUI/AppThemes/{ThemeName}.xaml")); + var xamlDictionaryFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri($"ms-appx://AppThemes/{ThemeName}.xaml")); return await FileIO.ReadTextAsync(xamlDictionaryFile); } } diff --git a/Wino.Mail.WinUI/Services/PreferencesService.cs b/Wino.Mail.WinUI/Services/PreferencesService.cs index 04da28bf..b6171b64 100644 --- a/Wino.Mail.WinUI/Services/PreferencesService.cs +++ b/Wino.Mail.WinUI/Services/PreferencesService.cs @@ -284,6 +284,12 @@ public class PreferencesService(IConfigurationService configurationService) : Ob set => SaveProperty(propertyName: nameof(WorkingDayEnd), value); } + public long DefaultReminderDurationInSeconds + { + get => _configurationService.Get(nameof(DefaultReminderDurationInSeconds), 900L); // Default: 15 minutes (900 seconds) + set => SaveProperty(propertyName: nameof(DefaultReminderDurationInSeconds), value); + } + public int EmailSyncIntervalMinutes { get => _configurationService.Get(nameof(EmailSyncIntervalMinutes), 3); diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarSettingsPage.xaml b/Wino.Mail.WinUI/Views/Calendar/CalendarSettingsPage.xaml index 4cb033fb..35b3a574 100644 --- a/Wino.Mail.WinUI/Views/Calendar/CalendarSettingsPage.xaml +++ b/Wino.Mail.WinUI/Views/Calendar/CalendarSettingsPage.xaml @@ -178,6 +178,16 @@ + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Views/Calendar/EventDetailsPage.xaml b/Wino.Mail.WinUI/Views/Calendar/EventDetailsPage.xaml index 14d2cdc6..be4932d5 100644 --- a/Wino.Mail.WinUI/Views/Calendar/EventDetailsPage.xaml +++ b/Wino.Mail.WinUI/Views/Calendar/EventDetailsPage.xaml @@ -6,6 +6,7 @@ xmlns:abstract="using:Wino.Mail.WinUI.Views.Abstract" xmlns:calendar="using:Wino.Core.Domain.Entities.Calendar" xmlns:calendarHelpers="using:Wino.Calendar.Helpers" + xmlns:calendarViewModels="using:Wino.Calendar.ViewModels" xmlns:coreControls="using:Wino.Mail.WinUI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:domain="using:Wino.Core.Domain" @@ -29,8 +30,10 @@