857 Commits

Author SHA1 Message Date
Burak Kaan Köse e0f517e993 Updated translations. 2026-04-20 23:20:08 +02:00
Burak Kaan Köse 66c556b587 Hide email address for calendar only accounts and fix startup crash. 2026-04-20 23:19:53 +02:00
Burak Kaan Köse 877fb0dbd4 Do not popup success sync message on executing requests. 2026-04-20 23:06:11 +02:00
Burak Kaan Köse 2ea65dc556 Remove the confirmamtion when compose page is dismissed after closing the window. 2026-04-20 22:51:20 +02:00
Burak Kaan Köse 23dce29ff8 Fix the issue with Chinese fonts on menu flyout items and appbar buttons in compose page. 2026-04-20 22:51:08 +02:00
Burak Kaan Köse 9292c963d5 Fix missing additional properties loading for mails. 2026-04-20 20:37:52 +02:00
Burak Kaan Köse 2b1676a4f7 Dispose mail webviews when closing the shell 2026-04-20 19:40:45 +02:00
Burak Kaan Köse d85812ed7b Add capability-first account and calendar setup flow 2026-04-20 19:38:37 +02:00
Burak Kaan Köse 54148716bb Fixing UI thread issues with bulk operations and request queue refactoring. 2026-04-20 02:18:23 +02:00
Burak Kaan Köse 3bd0b69429 Imap flow. 2026-04-19 20:13:09 +02:00
Burak Kaan Köse 496c7735f7 Add configurable thread item sorting 2026-04-19 16:26:30 +02:00
Burak Kaan Köse bfbc3d40b3 Make system tray icon optional 2026-04-19 10:47:42 +02:00
Burak Kaan Köse 9cc6b03f61 beta release 2.0.4 2026-04-19 01:49:29 +02:00
Burak Kaan Köse 00437bae4e Add local JSON account import and export 2026-04-18 15:55:15 +02:00
Burak Kaan Köse 2a93600ede Add junk folder empty action and refresh package versions 2026-04-18 15:34:14 +02:00
Burak Kaan Köse 90cb5f86b3 FlyoutPickerTitleTextBlockStyle overrides. 2026-04-18 01:44:33 +02:00
Burak Kaan Köse 3caecc5a9c Chinese language treatment for WinUI bug. 2026-04-18 01:39:25 +02:00
Burak Kaan Köse 9e735c911c New git ignore for translation trigger. 2026-04-18 01:39:02 +02:00
Burak Kaan Köse 37e60bdd6e Potential focus crashing on replying. 2026-04-18 01:38:11 +02:00
Burak Kaan Köse 85ac7e1330 Fix Translation updates. 2026-04-18 01:12:37 +02:00
Burak Kaan Köse d287c22725 Folder management stuff. 2026-04-18 00:02:54 +02:00
Burak Kaan Köse 98eed39fe6 Add per-account folder customization page (#855)
Introduce a dedicated settings page that lets users reorder, hide,
and pin/unpin folders per account. Folders are organized into Pinned,
Categories (Gmail only), and More sections with drag-to-reorder via
ListView. New Order column on MailItemFolder persists the custom
layout; the default sort falls back to alphabetic when no custom
order is set. A reset action wipes all customization in a single
transaction and restores system-folder stickiness.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-16 14:07:17 +02:00
Burak Kaan Köse 0b136b3d66 Fixing double root folder creation. 2026-04-16 14:06:14 +02:00
Burak Kaan Köse e1f53c7f9f Add account context menu actions 2026-04-16 13:47:41 +02:00
Burak Kaan Köse 784144cd13 Couple fixes for beta. 2026-04-16 13:45:11 +02:00
Burak Kaan Köse 65f7e0236a Bump version. 2026-04-16 02:04:42 +02:00
Burak Kaan Köse e13aaadc78 Fix notification activation and calendar bootstrap flow 2026-04-16 01:32:48 +02:00
Burak Kaan Köse 94675eee9a Remove winui build workflow. 2026-04-15 19:57:00 +02:00
Burak Kaan Köse b3360ecd76 Merge configurable mail notification actions 2026-04-15 15:54:07 +02:00
Burak Kaan Köse 4ca26cb131 Add configurable mail notification actions 2026-04-15 15:43:07 +02:00
Burak Kaan Köse 7c0f8d4bb4 Handle Outlook accounts without aliases 2026-04-15 13:07:58 +02:00
Burak Kaan Köse 999d8cde73 Preserve Gmail user label casing 2026-04-15 12:16:03 +02:00
Burak Kaan Köse 1a1d69be56 New beta release workflow. 2026-04-15 04:24:04 +02:00
Burak Kaan Köse c2540926f4 Fix an issue with "CommunityToolkit.WinUI.Extensions" package was restored using labsFeed instead of Nuget. 2026-04-15 04:23:52 +02:00
Burak Kaan Köse 9424fd9a16 Testing package version. 2026-04-15 04:01:47 +02:00
Burak Kaan Köse 89b48d3ac4 Moer updates on beta release pipeline. 2026-04-15 04:01:25 +02:00
Burak Kaan Köse 0bcc7a7647 Use existing version from the branch. 2026-04-15 03:37:47 +02:00
Burak Kaan Köse 260e1ab935 RID issue fix. 2026-04-15 03:17:34 +02:00
Burak Kaan Köse ccf7c0607b Remove cert password. 2026-04-15 03:06:18 +02:00
Burak Kaan Köse b8ce7e7422 Fix the incorrect feed for community toolkit extension thing. 2026-04-15 02:52:38 +02:00
Burak Kaan Köse 1365e42fd7 Explicit nuget.config for the workflow. 2026-04-15 02:46:06 +02:00
Burak Kaan Köse 0f160545ab Changelog. 2026-04-15 02:26:54 +02:00
Burak Kaan Köse 8481a5c7cd Better changelog handling for beta release workflow. 2026-04-15 02:24:11 +02:00
Burak Kaan Köse d32745fd67 Merge branch 'codex/manual-beta-release' 2026-04-15 02:14:52 +02:00
Burak Kaan Köse 470b2b8638 Add manual beta release workflow 2026-04-15 02:14:44 +02:00
Burak Kaan Köse 7e1731f4dc Fix compose initial focus behavior 2026-04-15 02:12:01 +02:00
Burak Kaan Köse aac9f9fec3 Merge branch 'codex/mail-categories-v1' 2026-04-15 01:18:12 +02:00
Burak Kaan Köse cf8fff8ef1 Add mail categories support 2026-04-15 01:18:07 +02:00
Burak Kaan Köse 0610096b78 Handle read-only calendars 2026-04-14 17:52:38 +02:00
Burak Kaan Köse feff929333 Bump version 2.0.1 - April 14. 2026-04-14 01:54:51 +02:00
Burak Kaan Köse aa16609f89 Add Windows share target draft attachment flow 2026-04-14 01:23:59 +02:00
Burak Kaan Köse 4bea53a667 Add custom theme deletion flow 2026-04-14 01:00:21 +02:00
Burak Kaan Köse b2ad4a1664 Fixing the Ui for reader & composer page. 2026-04-14 00:36:35 +02:00
Burak Kaan Köse dad3a51885 Remove debug print on preferences service. 2026-04-14 00:29:39 +02:00
Burak Kaan Köse 59ff0a1d7d Fix focused inbox not updating. 2026-04-14 00:29:29 +02:00
Burak Kaan Köse df19ab3196 Self contained. 2026-04-14 00:11:26 +02:00
Burak Kaan Köse c622858d2d Add initial mail sync range selection 2026-04-14 00:03:58 +02:00
Burak Kaan Köse 2e36772a4c Imap setup simplified and fixed the threading issues. 2026-04-13 23:11:35 +02:00
Burak Kaan Köse a2acad9ea4 Fix s/mime vertical combo on aliases. 2026-04-13 01:17:56 +02:00
Burak Kaan Köse 40b15b4f08 Improve alias capability model and Outlook alias sync 2026-04-13 01:09:45 +02:00
Burak Kaan Köse 6fd66810e9 Add email templates back to the settings menu. 2026-04-13 01:08:44 +02:00
Burak Kaan Köse 758a186c26 Remove test notification from event details. 2026-04-12 20:53:07 +02:00
Burak Kaan Köse b6bf5f2cd1 Remove new contact fromm menu list. 2026-04-12 20:12:48 +02:00
Burak Kaan Köse 3977401057 join online notification resolver fix. 2026-04-12 19:42:50 +02:00
Burak Kaan Köse 10c797fba4 Remove progress ring from accoount menu item 2026-04-12 19:42:27 +02:00
Burak Kaan Köse d922dd2f2e Fix online search dedupe and pane layout scrolling 2026-04-12 15:56:31 +02:00
Burak Kaan Köse 4d04595d0a Somem Ui issues. 2026-04-12 15:55:40 +02:00
Burak Kaan Köse 4ac3ca3ee4 Default open pane length size. 2026-04-12 01:53:37 +02:00
Burak Kaan Köse 678245d1fa Small UI adjustment for account menu progress. 2026-04-12 01:38:18 +02:00
Burak Kaan Köse c8ab214651 Merge read receipt tracking work 2026-04-11 21:03:22 +02:00
Burak Kaan Köse 230039cb57 Add read receipt tracking for sent mail 2026-04-11 21:02:51 +02:00
Burak Kaan Köse 448ebd6036 Respect API calendar colors unless overridden 2026-04-11 15:18:35 +02:00
Burak Kaan Köse e206368801 Migrate mail printing to WinUI print preview 2026-04-11 15:07:25 +02:00
Burak Kaan Köse 24626d1c31 Fix AppNotification multi-entry notifications 2026-04-11 13:02:44 +02:00
Burak Kaan Köse 5cb49efeb4 Updated synchronization progress implementation. 2026-04-11 13:00:56 +02:00
Burak Kaan Köse 40318ef99c Optimize mail list selection and draft pruning 2026-04-11 10:54:21 +02:00
Burak Kaan Köse fdb340549d Restore dual mail and calendar app entries 2026-04-11 01:28:25 +02:00
Burak Kaan Köse 4cb08f0a98 Implemented initial version for popping out window for rendering and compose pages 2026-04-11 01:05:08 +02:00
Burak Kaan Köse d5c121ce24 Fixing missing immamges from MSIX bundle. 2026-04-10 19:39:56 +02:00
Burak Kaan Köse be71ef3611 Fix packaging errors and revert test code. 2026-04-10 14:50:53 +02:00
Burak Kaan Köse 2a0d15ad69 Fixing notification activations. 2026-04-09 16:08:33 +02:00
Burak Kaan Köse a8310f1dab Fixing the leak in calendar mmode. 2026-04-09 01:26:23 +02:00
Burak Kaan Köse aaf0b7d069 Better creation of context menu items for calendar events. 2026-04-09 01:01:28 +02:00
Burak Kaan Köse 832a4b0348 Fixing couple issues with context flyout and moving items. 2026-04-09 00:17:30 +02:00
Burak Kaan Köse d6049bbd88 Calendar item context flyout implementation 2026-04-08 23:48:17 +02:00
Burak Kaan Köse 3dc4ac03ec Initial feature for drag / drop calendar events. 2026-04-08 23:46:02 +02:00
Burak Kaan Köse a3c35dfae5 Fixed an issue where updated outlook events turn into text instead of html. 2026-04-08 19:52:01 +02:00
Burak Kaan Köse cbdcfeae05 Missing translations. 2026-04-08 15:46:24 +02:00
Burak Kaan Köse 58a4e677e4 Agents.md update 2026-04-08 15:46:15 +02:00
Burak Kaan Köse 76f6ae0a1e - Fix for gmail calendar event creation.
- Proper junk API calls for gmail and outlook, not just moving the item.
- Add ability to hide ai actions panel.
2026-04-08 15:31:14 +02:00
Burak Kaan Köse a855d8c8a8 Gmail calendar actions fix. 2026-04-08 13:09:04 +02:00
Burak Kaan Köse 1567d9fa5e Fix some rendering issues in calendar with all day events. 2026-04-07 21:49:23 +02:00
Burak Kaan Köse 71fc883e47 Immidiate ui reflection for calendar events and some more error handling. 2026-04-07 16:48:46 +02:00
Burak Kaan Köse 3db54023a4 better 404 handling. 2026-04-07 13:23:07 +02:00
Burak Kaan Köse a9fd624742 2 second webview cache. 2026-04-07 09:52:37 +02:00
Burak Kaan Köse 9855170b2e Fixing issues with replies. 2026-04-07 01:17:52 +02:00
Burak Kaan Köse 12acff3bf8 Allow unsafe. 2026-04-07 00:25:16 +02:00
Burak Kaan Köse f693299304 Get rid of system.drawing and uwp notifications pacakge. Remove the AOT/trim stuff for now. 2026-04-07 00:02:36 +02:00
Burak Kaan Köse ff05195416 Better thread handling for mail collection. 2026-04-06 11:21:51 +02:00
Burak Kaan Köse c8265e75be Remove paddle billing callbacks. 2026-04-06 00:27:57 +02:00
Burak Kaan Köse 81e476e699 Gmail calendar sync fix. 2026-04-06 00:27:36 +02:00
Burak Kaan Köse 3357f6273c Public wino api service. 2026-04-05 16:56:32 +02:00
Burak Kaan Köse 323bbf7ea3 Resolving conflicts. 2026-04-05 16:53:51 +02:00
Burak Kaan Köse ef85ce6947 Merged feature/vNext. Initial commit for Wino Mail 2.0 2026-04-05 16:30:26 +02:00
Burak Kaan Köse 748ac8377a Bulk translation script usage. 2026-04-05 16:25:48 +02:00
Burak Kaan Köse 08bb9ede2a AI translations. 2026-04-05 16:23:20 +02:00
Burak Kaan Köse 32d677025d Cleanup old version service for maintenance. 2026-04-05 15:19:14 +02:00
Burak Kaan Köse ac78cf2b78 Remove GitHub Packages setup section from README
Removed GitHub Packages setup instructions from README.
2026-04-05 15:08:51 +02:00
Burak Kaan Köse 5f64cca518 Publich contracts package. 2026-04-05 14:14:13 +02:00
Burak Kaan Köse ca19297b92 Some more cleanup. 2026-04-05 13:18:50 +02:00
Burak Kaan Köse c1ab49fb1d Snooze with background activation. 2026-04-05 02:19:11 +02:00
Burak Kaan Köse 6013865fca Fix build error. 2026-04-04 20:34:55 +02:00
Burak Kaan Köse 1d0fcfb5b0 Import functionality for wino accounts, calendar sync UI, bunch of shell improvements 2026-04-04 20:23:20 +02:00
Burak Kaan Köse 1667aa34db AI action panel improvements. 2026-04-04 01:34:57 +02:00
Burak Kaan Köse 1211e9b28a New AI actions panel. Replaced new command bar. 2026-04-03 19:50:52 +02:00
Burak Kaan Köse 27e91316d3 Translation caching. New ai actions panel. 2026-04-03 11:56:25 +02:00
Burak Kaan Köse 8f16f553f5 Handling of AI pack through mmicrosoft store. 2026-04-02 15:07:05 +02:00
Burak Kaan Köse 7b369201b0 General account details settings and some marking mail issues 2026-04-01 01:41:17 +02:00
Burak Kaan Köse 6f61605c12 Handling of all day events and auto calendar sync on account creation. 2026-03-28 01:44:12 +01:00
Burak Kaan Köse 686446937b Disable aot for tests. 2026-03-27 21:00:02 +01:00
Burak Kaan Köse fb8a3d8f90 Handling some warnings and proper disposals of shells etc. 2026-03-27 14:45:36 +01:00
Burak Kaan Köse 3712041689 Some configs. 2026-03-27 12:58:41 +01:00
Burak Kaan Köse 37afb990f1 Better top shell account icon. 2026-03-27 12:58:25 +01:00
Burak Kaan Köse a465545fcb Live url 2026-03-27 12:58:08 +01:00
Burak Kaan Köse 022ffc567b AOT compatible signature and encryption page. 2026-03-27 12:57:44 +01:00
Burak Kaan Köse e3c3b341e5 Calendar improvements cycle 2 2026-03-25 15:49:14 +01:00
Burak Kaan Köse 8c492bb094 Calendar rendering improvements. 2026-03-25 13:39:27 +01:00
Burak Kaan Köse 0056f372b9 Fix search and global title bar issues. 2026-03-25 09:45:49 +01:00
Burak Kaan Köse 7aad6b0157 Wiring up the AI calls. 2026-03-25 00:25:05 +01:00
Burak Kaan Köse fd81ee31ce Tests 2026-03-24 23:20:41 +01:00
Burak Kaan Köse 27c90d2f89 Back navigation and shell improvements. 2026-03-24 18:05:09 +01:00
Burak Kaan Köse d699818c6f ShellContent improvements. 2026-03-24 16:57:13 +01:00
Burak Kaan Köse 317cad2459 Ai contracts update and special nav item for wino accounts. 2026-03-24 01:34:54 +01:00
Burak Kaan Köse ff84d62196 Settings refactoring. 2026-03-24 01:18:06 +01:00
Burak Kaan Köse 5c7f6aa734 Good improvements on the calendar. 2026-03-23 23:31:26 +01:00
Burak Kaan Köse 1adba271e2 Calendar rendering implementation. 2026-03-23 14:56:36 +01:00
Burak Kaan Köse 8586d0ef54 Calendar rendering. 2026-03-23 10:22:47 +01:00
Burak Kaan Köse 8d143e3b08 Remove old shells, some UI improvements for settings. 2026-03-21 18:25:54 +01:00
Burak Kaan Köse e6a38a3e77 New default style for calendar view. 2026-03-21 10:12:00 +01:00
Burak Kaan Köse 51fef043ee Range thing. 2026-03-21 00:58:01 +01:00
Burak Kaan Köse 01f7a09cb7 Merged 2026-03-20 13:26:45 +01:00
Burak Kaan Köse eb8cd7651d Calendar buttons etc. 2026-03-20 13:26:16 +01:00
Burak Kaan Köse c88c875fb8 Redesign Wino Account flyout with hero header and benefit cards 2026-03-20 13:12:11 +01:00
Claude a00af1da3f Redesign Wino Account flyout menu with Windows 11 Fluent Design
Replaces the plain signed-out flyout with a polished layout featuring:
- Gradient hero header with account icon and descriptive text
- Two benefit cards (sync settings, unlock add-ons) matching dialog patterns
- Equal-width Sign In (accent) and Register buttons
- Redesigned signed-in view with gradient profile header and larger avatar
- New translation keys for flyout benefit card content

https://claude.ai/code/session_019YP6uNe52Wo3SMuqz6w8QE
2026-03-20 12:10:15 +00:00
Burak Kaan Köse 1fe569e0ac Native tray icon implementation. 2026-03-20 12:43:09 +01:00
Burak Kaan Köse 4a20ea2577 Better profile caching. 2026-03-20 00:15:10 +01:00
Burak Kaan Köse d38317f0be Point real api. 2026-03-19 16:42:36 +01:00
Burak Kaan Köse c2320de5c4 Forgot password and email confirmations. 2026-03-19 16:41:35 +01:00
Burak Kaan Köse 873a7eca12 Bump contracts. 2026-03-19 14:32:12 +01:00
Burak Kaan Köse c3e1991942 Deep link on purchase success. 2026-03-19 10:26:17 +01:00
Burak Kaan Köse b0ee5c9974 Handling of paddle purchases and add-ons. 2026-03-19 01:50:14 +01:00
Burak Kaan Köse f306f6eb1c More updates on wino acc. 2026-03-18 17:43:56 +01:00
Burak Kaan Köse a3b43fd079 Add privacy policy for wino accounts when registering. 2026-03-18 14:45:00 +01:00
Burak Kaan Köse bac291587d New management page. 2026-03-18 10:25:07 +01:00
Burak Kaan Köse aee32228c2 Wino accounts settings. 2026-03-18 09:00:26 +01:00
Burak Kaan Köse 0d6da30a29 Bump contracts. 2026-03-17 17:48:47 +01:00
Burak Kaan Köse 289d0c8eeb Replace bland "W" initials with accent-colored person icon for signed-out state (#836)
When no Wino account is logged in, the titlebar button now shows a
filled accent-blue circle with a white person silhouette icon instead
of a PersonPicture with "W" initials. This makes the button visually
prominent and clearly communicates it's an account action. When signed
in, the PersonPicture with user initials is shown as before.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 17:47:45 +01:00
Burak Kaan Köse 4a94dfb10c Redesign Wino Account login and registration dialogs (#835)
Add hero illustrations using XAML-native vector graphics (shield+lock
for login, person+plus badge for registration). Improve visual design
with gradient backgrounds, icon badges on benefit cards, and refined
spacing/corner radii. Add Enter key handling so pressing Enter in an
input field moves focus to the next field, and pressing Enter on the
last field triggers the primary action (login/register).

https://claude.ai/code/session_011B1M6UVeo4yUX3zNHBxQ2P

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-17 16:08:04 +01:00
Burak Kaan Köse ea204fef21 Add preference to hide title bar Wino account button (#833) 2026-03-17 12:00:34 +01:00
Burak Kaan Köse e2b9216f8f New base url. 2026-03-17 02:00:53 +01:00
Burak Kaan Köse 5f519f6ae1 Fixing back navigation. 2026-03-16 21:41:22 +01:00
Burak Kaan Köse 5b2a99ffe5 Dialog improvements. 2026-03-16 15:02:39 +01:00
Burak Kaan Köse 4519b77444 Document GitHub Packages setup 2026-03-16 13:32:59 +01:00
Burak Kaan Köse 59d9cf4eea Consume Wino.Mail.Contracts package 2026-03-16 12:12:13 +01:00
Burak Kaan Köse 0ee3a0c3bc Delete instructions. 2026-03-16 11:23:13 +01:00
Burak Kaan Köse 37c1bd3f62 Sign in , out ,register. 2026-03-16 01:33:27 +01:00
Burak Kaan Köse 921c3bef93 Some ui adjustments on settings. 2026-03-14 22:37:54 +01:00
Burak Kaan Köse 45142e6953 Settings home refactoring. 2026-03-14 21:03:52 +01:00
Burak Kaan Köse 642f6efbfb Fix active canvas null issue. 2026-03-14 18:25:28 +01:00
Burak Kaan Köse 56b0f79edc navigation improvements 2026-03-14 14:14:58 +01:00
Burak Kaan Köse 4ba7d5fd07 Fixing menu item scrolling issue with nav bar. 2026-03-13 07:20:37 +01:00
Burak Kaan Köse 7f0b671b62 Settings shell. 2026-03-12 19:04:47 +01:00
Burak Kaan Köse de5309ea56 Finalized new shell experience. 2026-03-12 14:55:07 +01:00
Burak Kaan Köse fd13f2eba5 Calendar crashes fix. 2026-03-12 12:10:23 +01:00
Burak Kaan Köse 861b991eee Shell improvements. 2026-03-12 11:28:41 +01:00
Burak Kaan Köse 9dd68fd62e Improved shell experience. 2026-03-11 19:26:37 +01:00
Burak Kaan Köse 2b523d64e8 New shell experience. 2026-03-11 01:39:32 +01:00
Burak Kaan Köse bf331dfeb3 Better shell 2026-03-10 16:50:16 +01:00
Burak Kaan Köse 9b567c4bac Go back to welcome page when the last account is removed. 2026-03-09 14:18:13 +01:00
Burak Kaan Köse 859a5bb117 Revise README with new logo and updated text
Updated logo and description in README.md, removed unused images.
2026-03-09 13:19:12 +01:00
Burak Kaan Köse 0d898d3de0 Remove website visual assets section from README (#830) 2026-03-09 13:15:42 +01:00
Burak Kaan Köse 44be3eb4f7 Some UI changes on settings. 2026-03-09 00:28:10 +01:00
Burak Kaan Köse 3e731967cd Remove edit account details page. 2026-03-08 21:21:34 +01:00
Burak Kaan Köse 8548257878 Fix udate update notes. 2026-03-08 18:40:43 +01:00
Burak Kaan Köse d9da326f0a Rename database. 2026-03-08 16:28:41 +01:00
Burak Kaan Köse d43e2b269a Fix tailored notification images issue. 2026-03-08 16:26:06 +01:00
Burak Kaan Köse 9d94badb95 Fix storage page navigation issue. 2026-03-08 16:25:53 +01:00
Burak Kaan Köse e4a224bd68 Emaıl templates. 2026-03-08 15:48:11 +01:00
Burak Kaan Köse 15400d4096 Improved keyboad shortcuts. 2026-03-08 13:21:42 +01:00
Burak Kaan Köse c1568d33e6 Live store update notifications. 2026-03-08 11:22:41 +01:00
Burak Kaan Köse a8f9b2d126 Calendar improvements. 2026-03-08 01:33:47 +01:00
Burak Kaan Köse 1da34080d1 Refactoring the html editor toolbar. 2026-03-07 23:33:25 +01:00
Burak Kaan Köse ebc35c3de8 Event creation. 2026-03-07 17:13:48 +01:00
Burak Kaan Köse d1f8163d72 Web editor refactoring and some calendar occurrence summary stuff. 2026-03-07 15:37:02 +01:00
Burak Kaan Köse 09f1cee3a5 Remove sqlite base64 contact store from AccountContact. 2026-03-07 11:43:56 +01:00
Burak Kaan Köse 8e8b123aa6 Updating some nugets. 2026-03-07 01:52:23 +01:00
Burak Kaan Köse 9ec7b32762 Merge branch 'feature/EventCompose' into feature/vNext 2026-03-07 01:46:21 +01:00
Burak Kaan Köse e94cce451f Event compose implementation. 2026-03-07 01:46:07 +01:00
Burak Kaan Köse 6608baed69 Initial event composing. 2026-03-06 17:46:38 +01:00
Burak Kaan Köse 59042729c1 "Outlook error" handling for 429 status code. 2026-03-06 13:43:16 +01:00
Burak Kaan Köse e1be644631 Contact and settings updates. 2026-03-06 12:31:37 +01:00
Burak Kaan Köse 51f64466c2 Updating core title bar for menu item changes. 2026-03-06 11:22:19 +01:00
Burak Kaan Köse 24f7c26d60 Visual refresh of dialogs. 2026-03-06 11:22:12 +01:00
Burak Kaan Köse 1aaf4e8a7e Settings UI 2026-03-06 04:04:14 +01:00
Burak Kaan Köse 3d6763770e merged 2026-03-06 03:43:06 +01:00
Burak Kaan Köse aaa6e8a2c9 Removed migrations. New onboarding screen and wizard like steps. 2026-03-06 03:42:08 +01:00
Burak Kaan Köse db5ecd60e4 New startup window. 2026-03-05 10:12:03 +01:00
Burak Kaan Köse 5b3739c6cf Add snooze support for calendar reminders (toast UI, service, DB) (#825)
* Filter reminder snooze options by default reminder

* Some updates.

* Fixing empty welcome page issue and attendee loading.

* Icon system for notifications and snooze options etc.
2026-03-04 00:12:52 +01:00
Burak Kaan Köse d45d3faa89 Whats new implementation. 2026-03-02 00:44:29 +01:00
Burak Kaan Köse e816e87f61 Contacts management. 2026-03-01 21:07:10 +01:00
Burak Kaan Köse bdd32786d6 folder structure fixes 2026-03-01 16:23:28 +01:00
Burak Kaan Köse f35a4333f9 Fix initials showing in the background when the profile picture has transparent background. 2026-03-01 13:48:40 +01:00
Burak Kaan Köse 2c9351f551 Fixing some IsBusy corner cases. 2026-03-01 12:40:12 +01:00
Burak Kaan Köse 211faff750 Property change based updates on the mails for fast bulk operations. 2026-03-01 12:07:15 +01:00
Burak Kaan Köse 11158fe737 Remove redundant notification target. 2026-03-01 09:50:05 +01:00
Burak Kaan Köse 76e3b7289e Some issues with changing the app mode and notifications have been fixed. 2026-03-01 09:47:05 +01:00
Burak Kaan Köse 2040d4abce Optimize mail fetching with batch DB queries and in-memory caching (#827)
* perf: batch-load folders, accounts, and contacts in FetchMailsAsync

Replace the sequential per-mail property-loading loop with a three-step
batch pre-load strategy, eliminating the N+1 DB call pattern that was
the main bottleneck when building the mail list with threading enabled.

Changes:
- Pre-seed the folder cache from MailListInitializationOptions.Folders
  so that the most common folders (inbox, sent, etc.) never trigger a DB
  lookup at all.
- Load all accounts in a single GetAccountsAsync() call instead of one
  GetAccountAsync() call per mail (typically 1–5 accounts total).
- Fetch all sender contacts in a single SQL IN(...) query via the new
  GetContactsByAddressesAsync() method instead of one query per address.
- Property assignment is now fully synchronous (no awaits in the loop)
  since all data is pre-loaded into plain Dictionary<K,V>.
- Thread-expansion follows the same pattern: new folder IDs are loaded
  in parallel via Task.WhenAll; new contact addresses are batch-fetched
  with a second IN(...) query.
- Also apply batch pre-loading to GetMailItemsAsync (used by merge-inbox
  sync path) which had the same sequential issue.
- Remove the now-unused LoadAssignedPropertiesWithCacheAsync helper and
  the ConcurrentDictionary dependency it required.
- Tighten GetMailsByThreadIdsAsync to skip the Id NOT IN clause entirely
  when the exclusion set is empty.

https://claude.ai/code/session_018bqahGc6zi95JJhc2aARKS

* test: add MailFetchingTests with correctness and performance coverage

Adds integration tests for MailService.FetchMailsAsync that exercise the
full real-service stack (MailService → FolderService / AccountService /
ContactService) backed by the shared in-memory SQLite helper.

Four tests are included:

• ExpandsSiblingsOutsidePage – proves thread expansion fetches mails that
  fall beyond the initial SQL page (6 mails, page=4, expects 6 returned).

• NeverExpandsSiblings – proves threading is truly opt-in; with
  CreateThreads=false the result exactly matches the raw page size.

• ResolvesFromAllThreeSources – verifies contact resolution for a known
  contact (from the AccountContact table), an unknown sender (ad-hoc
  fallback), and a self-sent mail (built from account metadata).

• 1000Mails_70Threads_CompletesWithinBudget – the performance scenario:
  1 000 mails (70 threads × 7 + 510 standalone), 40 rotating sender
  addresses (20 with DB contacts). Times and reports two scenarios:
    - Default first-page fetch (100 mails) + expansion of one partial
      thread (expects > 100 mails returned).
    - Full load of all 1 000 mails with threading enabled (expects
      exactly 1 000 mails returned, all 70 threads intact, < 5 s).

  Elapsed times for both scenarios are written to xUnit test output so
  they appear in CI logs and can be tracked across builds.

https://claude.ai/code/session_018bqahGc6zi95JJhc2aARKS

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-01 09:14:02 +01:00
Burak Kaan Köse 0e742c7a8f Resolving warnings and treating warnings as errors in WinUI project. (#824) 2026-02-27 20:12:43 +01:00
Burak Kaan Köse d2fce5eee1 Add PR GitHub Actions workflow to build WinUI and run Core tests on Windows (#823) 2026-02-27 14:16:31 +01:00
Burak Kaan Köse 5c510fd7b0 Remove single entry and mode launch on ctrl press. 2026-02-27 11:00:25 +01:00
Burak Kaan Köse e1ce85698c Fix couple issues with starting mode. 2026-02-27 10:22:52 +01:00
Burak Kaan Köse 4b22608bc5 Badges are always creatd on wino mail. 2026-02-25 02:05:05 +01:00
Burak Kaan Köse 3a39266121 Simplified compoper and rendering logic through messages. 2026-02-25 01:41:48 +01:00
Burak Kaan Köse 5d46ea73db Route mail/calendar toasts to their respective app entries (#821) 2026-02-25 01:36:26 +01:00
Burak Kaan Köse d51f4a7a23 Add SQLite indexes and enable foreign key enforcement (#820) 2026-02-24 11:08:46 +01:00
Burak Kaan Köse 79a81710f0 Improving thread mapping for all synchronizers. 2026-02-23 01:51:44 +01:00
Burak Kaan Köse c5a631da6f Grace period for local drafts. 2026-02-23 01:02:59 +01:00
Burak Kaan Köse 33672ab0aa Local draft resent and default app mode settings. 2026-02-22 17:55:57 +01:00
Burak Kaan Köse 311b3c77c8 Wino Calendar app entry. (#819)
* Double app entry.

* New icon for sys tray
2026-02-22 15:13:39 +01:00
Burak Kaan Köse 17ca32c537 Support large Outlook attachments via upload sessions when sending drafts (#814)
* Add Outlook large attachment upload sessions for send draft

* UI thread executino of draft busy state.

* Limit outlook attachment limit to max allowed per attachment.
2026-02-21 16:14:55 +01:00
Burak Kaan Köse 9d3f0bddde Add manual live ImapSynchronizer coverage tests (#818)
* Add manual live IMAP synchronizer tests

* Fixing build errors and testing.
2026-02-21 11:47:16 +01:00
Burak Kaan Köse 7f198bad92 Implement mail and calendar item synchronizer state (#815)
* Track pending sync operations per mail/calendar item

* Updated progressbar for in progress drafts
2026-02-21 10:53:39 +01:00
Burak Kaan Köse a912ada890 Fixing some messaging issues with calendar add/delete. 2026-02-20 10:03:16 +01:00
Burak Kaan Köse 317113a1b3 Fixing timezone issue with caldav. 2026-02-19 02:09:36 +01:00
Burak Kaan Köse 564cb0b16f Fix double init on calendar days. 2026-02-19 01:37:43 +01:00
Burak Kaan Köse ab0810f710 Fixing the delta sync for caldav. 2026-02-18 20:43:10 +01:00
Burak Kaan Köse 7a13ae0ac8 Add manual live CalDAV service workflow tests (#816) 2026-02-18 13:44:58 +01:00
Burak Kaan Köse c8e1678e55 Fix HtmlPreviewVisitor regressions and add sanitization tests (#813) 2026-02-17 22:12:27 +01:00
Burak Kaan Köse f49d276f5a Add dedicated Wino.Mail.ViewModels.Tests coverage for WinoMailCollection (#812)
* Add WinoMailCollection tests in dedicated ViewModels test project

* Fix WinoMailCollection tests flattening without SelectMany
2026-02-17 15:45:29 +01:00
Burak Kaan Köse 05112d6a35 Dispatch WebView2 runtime toast notification on UI thread (#811) 2026-02-16 16:32:47 +01:00
Burak Kaan Köse fec49ce6f8 Proper cleanup of account on the UI when its deleted. 2026-02-16 01:56:22 +01:00
Burak Kaan Köse 31a7faeef9 Handle operation execution errors in rendering page. 2026-02-16 01:39:53 +01:00
Burak Kaan Köse dae7d046c4 Calendar metadata fetch after creating account. 2026-02-15 19:57:48 +01:00
Burak Kaan Köse d428a6ce7a Ignore local calendar applying changes to prevent duplicate operations. 2026-02-15 19:44:07 +01:00
Burak Kaan Köse ff25db3fea Add busy state support for calendar item view models (#810) 2026-02-15 19:26:06 +01:00
Burak Kaan Köse 2baa87daeb Add IMAP local calendar operation tests using in-memory DB (#807)
* Add IMAP local calendar operation handler tests

* Fix tests.

* Fix calendar item show as not updating.

* Create one default calendar for local calendar accounts.
2026-02-15 18:40:32 +01:00
Burak Kaan Köse 42e51571a8 Bunch of calendar implementation thing. 2026-02-15 11:27:30 +01:00
Burak Kaan Köse acf0f649e8 CalDav synchronizer, new IMAP setup/edit page. 2026-02-15 02:20:18 +01:00
Burak Kaan Köse 64b9bfc392 Flag changes for uid based imap sync 2026-02-14 13:22:16 +01:00
Burak Kaan Köse 744145be06 Refactored impa synchronization. 2026-02-14 12:52:17 +01:00
Burak Kaan Köse 4a0dcd2899 Remove old project files. 2026-02-13 20:45:55 +01:00
Burak Kaan Köse 92df726f34 Batch flip-view range updates for programmatic calendar navigation (#805)
* Batch calendar range updates during programmatic navigation

* Refine programmatic calendar navigation batching state
2026-02-13 14:37:24 +01:00
Burak Kaan Köse dbd5812c45 Fix null handling in WinoCalendarView date range updates (#806) 2026-02-13 10:58:25 +01:00
Burak Kaan Köse 884f000058 Calendar stuff. 2026-02-13 03:09:13 +01:00
Burak Kaan Köse e936c431a2 Search improvements. 2026-02-12 18:57:55 +01:00
Burak Kaan Köse b01fa4e4ba Event details page improvements, calendar item update source. 2026-02-12 18:04:29 +01:00
Burak Kaan Köse 96dcdc8e03 Auto sync trigger and cancellation support. 2026-02-11 14:50:59 +01:00
Burak Kaan Köse 96d2efb3f0 Remove semantic zoom support. 2026-02-11 14:50:48 +01:00
Burak Kaan Köse 37199d84cb Fixed the caching issue that causes mails to be not removed. Improved drag/drop. 2026-02-11 11:34:50 +01:00
Burak Kaan Köse 52ee5f1d8a UI visuals for mail calendar items, calendar reminders. 2026-02-11 01:49:29 +01:00
Burak Kaan Köse 870a5e2bf6 Calendar - mail mapping. 2026-02-10 21:35:55 +01:00
Burak Kaan Köse 10dd42b63f Thread UI fixes. 2026-02-10 01:03:03 +01:00
Burak Kaan Köse 0999c71578 Contacts, thread animation and image preview control improvements. 2026-02-09 22:39:30 +01:00
Burak Kaan Köse e559a79506 Generic 404 handler for synchronizers. 2026-02-08 22:20:38 +01:00
Burak Kaan Köse 1747ed84a8 Disable logging synchronizer exceptions to sentry. 2026-02-08 19:43:13 +01:00
Burak Kaan Köse 22c6452227 Editor optimizations 2026-02-08 10:35:24 +01:00
Burak Kaan Köse ad9b94d407 Removed the INC registrations for list view items. 2026-02-08 01:41:32 +01:00
Burak Kaan Köse 9f13bcd991 Collection optimizations. 2026-02-08 01:41:09 +01:00
Burak Kaan Köse 5bfa61a218 Create sub folder, delete folder, storage settings, some ui adjustments on threads. 2026-02-07 19:47:21 +01:00
Burak Kaan Köse 2cd03d5fec Fix unrealized container unselected issue with the threads. 2026-02-07 15:50:23 +01:00
Burak Kaan Köse c7fb648387 Thread selection improvements 2026-02-07 15:29:19 +01:00
Burak Kaan Köse 331b966556 Info panel for synchronizers in shell. 2026-02-07 14:03:41 +01:00
Burak Kaan Köse d28de50ec6 Fixing outlook attachments, re-using compose page and some additional fixes on the mime headers for outlook. 2026-02-07 13:10:57 +01:00
Burak Kaan Köse 1ec8d5bbf2 Gmail drafting 2026-02-06 21:46:30 +01:00
Burak Kaan Köse 4374d19ac2 Threading improvements. 2026-02-06 20:13:44 +01:00
Burak Kaan Köse 071f1c9786 Refactored all synchronizers to deal with some of the chronic issues. 2026-02-06 01:18:12 +01:00
Burak Kaan Köse d1425ca9ca Ignore claude permissions. 2026-02-06 01:18:10 +01:00
Burak Kaan Köse 2fd600d47d Partial Busy state for mark as read requests 2026-02-05 12:48:38 +01:00
Burak Kaan Köse 0eba778158 Mail update source. 2026-01-27 21:21:04 +01:00
Burak Kaan Köse b343152f14 Some experiments. 2026-01-27 20:37:18 +01:00
Wynn Zeng 4f65502c95 fix(ui): batch UI updates for bulk mark-as-read to prevent UI freeze (#786) 2026-01-27 20:25:05 +01:00
Burak Kaan Köse 31097e42a9 Reacting calendar changes 2026-01-20 00:30:24 +01:00
Burak Kaan Köse 319b0af305 Global back listener for mouse. 2026-01-06 17:34:06 +01:00
Burak Kaan Köse f105c2f8f0 Settings page and manage accounts navigation options. 2026-01-06 17:23:58 +01:00
Burak Kaan Köse 7cc201f423 ShowAs stripe for calendar control template. 2026-01-06 12:54:47 +01:00
Burak Kaan Köse a23a99cc8d Join online for quick popup. 2026-01-06 12:07:22 +01:00
Burak Kaan Köse be6b23c47b AOT safe panels. 2026-01-06 11:45:03 +01:00
Burak Kaan Köse f8333aab10 Single isntances and some updates shit. 2026-01-06 11:11:37 +01:00
Burak Kaan Köse d279c0a8dd Fix syncing ocurrences. 2026-01-05 15:10:37 +01:00
Burak Kaan Köse bd8867dba6 Potential nre. 2026-01-05 15:10:33 +01:00
Burak Kaan Köse 3d07328f47 Calendar invitations for Mail part of the app. 2026-01-05 00:21:07 +01:00
Burak Kaan Köse 0b0f6b8d8e Show as localization. 2026-01-04 13:25:08 +01:00
Burak Kaan Köse 4603b1fb14 Calendar attachments. 2026-01-03 23:59:37 +01:00
Burak Kaan Köse c8ef031e7d Some changes. 2026-01-03 20:46:03 +01:00
Burak Kaan Köse 9877656eea RSVP options. 2026-01-03 19:33:36 +01:00
Burak Kaan Köse a64627e7d6 Reminders. 2026-01-01 15:02:40 +01:00
Burak Kaan Köse 3b485dc1fe Event details UI improvements. 2026-01-01 10:07:56 +01:00
Burak Kaan Köse e71c050724 Item update and delete scenarios. 2025-12-31 15:33:13 +01:00
Burak Kaan Köse d54a9f6279 copilot instructions 2025-12-31 14:15:12 +01:00
Burak Kaan Köse f917e4a721 New setting options. 2025-12-31 14:09:57 +01:00
Burak Kaan Köse 61fb10a951 Calendar settings on settings page. 2025-12-31 13:28:53 +01:00
Burak Kaan Köse d3704a0f09 More coverage for Esc to unselect all items. 2025-12-31 11:08:30 +01:00
Burak Kaan Köse c584929db5 Fix outlook sync. 2025-12-30 23:49:25 +01:00
Burak Kaan Köse ea4cf20746 +2 years sync for Outlook 2025-12-30 23:41:53 +01:00
Burak Kaan Köse 2056a2d783 Handle deleted events. 2025-12-30 23:32:00 +01:00
Burak Kaan Köse b81ab0ca15 Creating events. 2025-12-30 11:59:54 +01:00
Burak Kaan Köse 70ac2d2bea New grouped collection for quick event dialog. 2025-12-30 10:36:27 +01:00
Burak Kaan Köse 07f3dabff6 win2d -> skia, some improvements on rendering. 2025-12-30 10:02:24 +01:00
Burak Kaan Köse 72e43e4b7a Recalculate recurrences when a new event added. 2025-12-30 08:51:50 +01:00
Burak Kaan Köse 0519bf86b3 I dont know some improvements on reacting calendar changes. 2025-12-29 23:13:32 +01:00
Burak Kaan Köse 6ba2f1f3e2 Make sure outlook correctly calls datetime for delta api. 2025-12-29 14:46:31 +01:00
Burak Kaan Köse 8613e92b31 Fixed the display date of the calendar items. Created test project for core library, included tests for recurring calendar events. 2025-12-29 14:10:09 +01:00
Burak Kaan Köse f79305f0a6 Fix the AOT issue with custom binding of IsSelected property through CVS in Mail List. 2025-12-28 07:28:20 +01:00
Burak Kaan Köse 0f6aa66b21 Small warning. 2025-12-28 07:27:27 +01:00
Burak Kaan Köse 51540c89d1 Fix auto nav to calendar on launch. 2025-12-28 06:58:06 +01:00
Burak Kaan Köse a5227abd40 Some UI shit. 2025-12-27 19:16:24 +01:00
Burak Kaan Köse 014b5aa671 Initial integration. 2025-12-26 20:46:48 +01:00
Burak Kaan Köse 10b85ea135 Fixing attachment icons. 2025-12-25 19:48:42 +01:00
Burak Kaan Köse f6e94e89c9 Fixing an issue where DeleteAsync calls expect PK. 2025-12-25 17:21:23 +01:00
Burak Kaan Köse 8a68fafedf Fixed an issue with loading mails with infinite scroll. 2025-12-15 21:06:13 +01:00
Burak Kaan Köse 7f8c6776fc New Crowdin updates (#783)
* New translations resources.json (Romanian)

* New translations resources.json (Spanish)

* New translations resources.json (Czech)

* New translations resources.json (Danish)

* New translations resources.json (Italian)

* New translations resources.json (Dutch)

* New translations resources.json (Polish)

* New translations resources.json (Chinese Simplified)

* New translations resources.json (Indonesian)
2025-12-15 21:05:53 +01:00
dayblox 6e5efa69c9 Fix typo in documentation comment (#782) 2025-12-15 21:05:17 +01:00
Burak Kaan Köse 9fbbd00dc5 Object deleted error fix. 2025-11-30 17:51:44 +01:00
Burak Kaan Köse a8a5d3c3d6 Fixed the issue with mail rendering page not getting disposed properly. 2025-11-24 20:54:57 +01:00
Maicol Battistini beb3bf9d1d feat: S/MIME signing and encryption (#693)
* feat: add S/MIME certificate management

- Introduced `ISmimeCertificateService` interface for managing S/MIME certificates.
- Implemented `SmimeCertificateService` class to handle certificate operations.
- Updated `WinoPage` enum to include `SignatureAndEncryptionPage`.
- Added resource entries in `resources.json` for S/MIME related messages.
- Created `SignatureAndEncryptionPage` view and logic for user interaction.
- Modified configuration files to integrate the new service and page.
- Updated project files to include necessary dependencies for certificate management.

* refactor(SmimeCertificateService): ♻️ Use constant for certificate name

Refactored the `SmimeCertificateService` to replace the hardcoded string "Wino Mail Certificate" with a constant `CertificateFriendlyName`. This change enhances code maintainability by centralizing the definition of the certificate's friendly name.

• Introduced a constant for the certificate's friendly name.
• Updated the certificate retrieval and import logic to use the new constant.

* feat(alias):  Add S/Mime certificate selection for every alias

Added new properties and methods in `MailAccountAlias` to manage signing and encryption certificates, including their thumbprints. This enhancement allows for better handling of S/Mime certificates within the application.

• Introduced new properties for signing and encryption certificates.
• Updated `resources.json` with new translations for S/Mime certificates.
• Enhanced `AliasManagementPageViewModel` to include a dependency on the S/Mime certificate service and updated alias loading methods.
• Modified `AliasManagementPage.xaml` to include ComboBox controls for selecting certificates.
• Implemented methods in `AliasManagementPage.xaml.cs` to handle certificate selection from dropdowns.

This change improves the user experience by allowing users to select and manage their S/Mime certificates directly within the alias management interface.

* feat(mail):  Add S/MIME support and file picker updates

Enhanced the `MailRenderModel` class by adding a new property `IsSmimeSigned` to indicate if an email is S/MIME signed. The constructor has been updated to accept `MailRenderingOptions`.

Updated the file selection logic in `DialogServiceBase` to replace the `FolderPicker` with a `FileSavePicker`, streamlining the process of saving files. Removed unnecessary commented code and added logic to handle file extensions.

In `MailRenderingPageViewModel`, a new property `IsSmimeSigned` reflects the S/MIME status of the current render model, along with a new method `ShowSmimeCertificateInfoAsync` to display S/MIME certificate details.

Added a `HyperlinkButton` in `MailRenderingPage.xaml` to indicate S/MIME status, which is only visible for signed emails, providing a tooltip and command for more information.

In `MimeFileService`, implemented logic to detect S/MIME signatures in messages and exclude S/MIME signature parts from attachments.

* refactor(viewmodel): ♻️ Replace dialog service messages

Refactored the `SignatureAndEncryptionPageViewModel.cs` to replace calls to `_dialogService.ShowMessageAsync` with `_dialogService.InfoBarMessage`. This change improves the handling of success messages during certificate import and removal processes.

* feat(mail):  Add S/MIME encryption indicator

Implemented support for S/MIME email handling in the MailRenderingPageViewModel. This includes the addition of a new property to check if an email is encrypted and updates to methods for displaying S/MIME certificate information.

A new column was added in the MailRenderingPage.xaml to indicate if an email is encrypted, along with updated tooltips and commands. The MimeFileService was also modified to detect S/MIME encryption and to exclude S/MIME signature certificates during attachment processing.

* fix: Added missing property

* feat: Added S/Mime decryption and signing verification and improvements

* i18n(resources): 🌐 Add S/MIME translation strings

Added new translation strings for S/MIME functionalities in `resources.json`, including messages for signatures and certificates in both English and Italian. The code has been updated to utilize these new translation strings, enhancing the application's internationalization.

Updated `MailRenderingPageViewModel.cs` to use the new translation strings for signature and certificate messages, improving code readability and consistency with translations. Additionally, the tooltips for S/MIME signing and encryption buttons in `MailRenderingPage.xaml` have been updated to use the new translation strings, enhancing the user experience for Italian-speaking users.

* fix: Extract body from MultipartSigned message

* feat(smime):  Enhance S/MIME certificate handling

Updated the `SmimeCertificateService` to improve the loading of PKCS12 certificate collections by adding `X509KeyStorageFlags.DefaultKeySet` and `X509KeyStorageFlags.Exportable` for better key management.

In `ComposePageViewModel`, imported necessary namespaces for S/MIME certificate handling and added a new dependency for `ISmimeCertificateService`. Implemented logic in `OpenAttachmentAsync` to load alias certificates and manage message signing and encryption based on user-selected certificates.

This change enhances the security and flexibility of email handling within the application.

* feat: Replaced Smime encryption certificate combobox with checkbox

Cert selection is useless for encryption

* feat: Added S/Mime togglebuttons when composing an email

* i18n(translations): 🌐 Add new composer translations

Added new translation strings for composer features, including themes, text formatting, and S/MIME signing and encryption options. Updated button labels to utilize these new strings, enhancing the application's internationalization.

Additionally, removed an obsolete string related to S/MIME certificate file information.

* Example for relay command and fix settings pages runtime error

* refactor(viewmodel): ♻️ Update certificate import/export commands

Refactored the certificate import and export commands in the `SignatureAndEncryptionPageViewModel`. Changed methods from `async void` to `async Task` for better error handling and tracking of asynchronous operations. Added `[RelayCommand]` attributes to improve adherence to the MVVM pattern.

Updated the XAML file to bind buttons directly to the new command methods, removing the need for event handlers. This enhances separation of concerns and simplifies the code.

Removed obsolete event handlers from the code-behind file, streamlining the implementation.

* fix: export folderPath parameter contains file name

* fix: QRESYNC initial modseq should be 1 (#734)

* Fix typo in reorder accounts dialog (#754)

* fix: Missing commas in translations files

* fix: merge issues

* Fix mege conflicts.

* Some more conflict fixes.

* Fixing context.

* Fixing saving file with suggested file name.

---------

Co-authored-by: Aleh Khantsevich <aleh.khantsevich@gmail.com>
Co-authored-by: Konstantin Shkel <null+github@pcho.la>
Co-authored-by: Cas Cornelissen <cas.cornelissen@onefinity.io>
Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2025-11-23 20:56:57 +01:00
Burak Kaan Köse 1a2590e2c3 Missing import. 2025-11-23 17:05:11 +01:00
Burak Kaan Köse 8858ef08c2 Better progress for outlook synchronizer. 2025-11-23 17:04:38 +01:00
Burak Kaan Köse 4520e16048 Make sure gmail sdk is trimmable. 2025-11-23 16:48:54 +01:00
Burak Kaan Köse 56cd29429e Remove Bindings for maill ist page. 2025-11-16 01:56:10 +01:00
Burak Kaan Köse 07aeaf8c8f Removal of Bindings 2025-11-16 00:23:23 +01:00
Burak Kaan Köse a2c7e5f29a Remove old projects. 2025-11-16 00:08:45 +01:00
Burak Kaan Köse b3130d9441 New assets. 2025-11-16 00:02:55 +01:00
Burak Kaan Köse 0dd907e314 Merge core project into winui project. 2025-11-15 14:52:01 +01:00
Burak Kaan Köse 12a39064dc Some item templates and removal of sqlkata. 2025-11-15 13:29:02 +01:00
Burak Kaan Köse b356af8eb4 Main app aot compatibility. 2025-11-14 18:51:48 +01:00
Burak Kaan Köse ae64094feb Make winui core library aot compatible. 2025-11-14 14:48:03 +01:00
Burak Kaan Köse 472cc3d7f2 Fix warnings for core view models. 2025-11-14 14:44:56 +01:00
Burak Kaan Köse dbaed6094b Make core library aot compatible. 2025-11-14 14:42:05 +01:00
Burak Kaan Köse 8cb8f27e00 Make services aot compatible. 2025-11-14 14:28:10 +01:00
Burak Kaan Köse d9ef81729f Enable aot for libs 2025-11-14 13:59:38 +01:00
Burak Kaan Köse d592d1c235 Nuget bump 2025-11-14 13:51:46 +01:00
Burak Kaan Köse e185301277 Fix missing window handler for outlook authenticator. 2025-11-14 13:29:49 +01:00
Burak Kaan Köse 249a950dc1 Fix soem dispaly date issues. 2025-11-14 12:56:37 +01:00
Burak Kaan Köse 540a4e5117 Fix single instancing. 2025-11-14 12:51:19 +01:00
Burak Kaan Köse 3d5da92c74 Revert debug code. 2025-11-14 12:51:12 +01:00
Burak Kaan Köse 88fe141b16 Handle attention in sync manager. 2025-11-14 12:31:24 +01:00
Burak Kaan Köse 87d2ffdb71 Remove test code 2025-11-14 12:31:13 +01:00
Burak Kaan Köse 13cb3a1042 Account attentions. 2025-11-14 12:12:13 +01:00
Burak Kaan Köse 6be271565e Toast actions. 2025-11-14 11:37:26 +01:00
Burak Kaan Köse 8482171bf2 Fixed the issue with single item context menus. 2025-11-12 18:52:15 +01:00
Burak Kaan Köse c277893145 Fixed selected style for single mail ittem list view item. 2025-11-12 18:52:03 +01:00
Burak Kaan Köse 9a0290d7a6 Handling actions on toast notifications when the app is running. 2025-11-12 18:51:53 +01:00
Burak Kaan Köse 777219ab87 Toast notification navigations and some improvements for list view selection. 2025-11-12 15:44:43 +01:00
Burak Kaan Köse 16e06af76f Fix unknown sender issue. 2025-11-12 00:39:37 +01:00
Burak Kaan Köse 3b776ec1bd Fixing selected item effect. 2025-11-10 01:47:05 +01:00
Burak Kaan Köse 175ed24a66 Some selections. 2025-11-09 21:36:07 +01:00
Burak Kaan Köse 5f9b51e4db Some threading stuff. 2025-11-01 21:46:23 +01:00
Burak Kaan Köse ae9e35e091 Fix the sorting when adding mails. 2025-11-01 12:35:47 +01:00
Burak Kaan Köse b60832a270 Get rid of the mail item queue system. Go back to 6 months initial sync strategy. 2025-11-01 12:11:05 +01:00
Burak Kaan Köse 5186b14905 Initialize the web editor. 2025-11-01 12:10:44 +01:00
Burak Kaan Köse 2a67a1e961 draft header 2025-11-01 01:04:04 +01:00
Burak Kaan Köse 4d0d2ff099 Graph rate limit handler. 2025-10-31 19:53:48 +01:00
Burak Kaan Köse 37b8a382a8 System icon. 2025-10-31 19:53:43 +01:00
Burak Kaan Köse f06273aa77 Thread safe collections. 2025-10-31 19:53:31 +01:00
Burak Kaan Köse 600d1b7d38 Retry downloading in batches for Outlook 2025-10-31 12:13:54 +01:00
Burak Kaan Köse 9e74fa9578 Fix the issue where sent and draft items are added to the existing folder regardless. 2025-10-31 11:26:51 +01:00
Burak Kaan Köse 282655dca8 Fix crash 2025-10-31 01:47:33 +01:00
Burak Kaan Köse 3cc1d10b87 some changes for progress 2025-10-31 01:41:51 +01:00
Burak Kaan Köse 4bf8f8b3d3 Bunch of improvements i dunno. 2025-10-31 00:51:27 +01:00
Burak Kaan Köse 2d81d07c0a Mail queues. 2025-10-30 17:15:05 +01:00
Burak Kaan Köse b0ac6e4e55 Demo contacts page. 2025-10-29 19:35:04 +01:00
Burak Kaan Köse 3db1fd0dde Cleanup main list view on page navigation. 2025-10-29 18:45:14 +01:00
Burak Kaan Köse df0eae256c Add copilot instructions. 2025-10-29 18:44:58 +01:00
Burak Kaan Köse 9c348f79d7 Add drag start in list view. 2025-10-29 18:44:49 +01:00
Burak Kaan Köse 525950a4da Fix for sender name and adress not updating in threads. 2025-10-29 18:44:38 +01:00
Burak Kaan Köse 394af3ba0a Gmail synchronizer improvements. 2025-10-29 18:44:15 +01:00
Burak Kaan Köse 27177acff7 Load more command for list view. 2025-10-29 17:29:42 +01:00
Burak Kaan Köse 864d68b6ac Publc partial 2025-10-29 17:03:17 +01:00
Burak Kaan Köse c2e6c68f50 Fixing modiufiers. 2025-10-29 17:02:58 +01:00
Burak Kaan Köse b44fb5c45a Keyboard shortcuts dialog. 2025-10-29 16:26:46 +01:00
Burak Kaan Köse abaab18eb7 Auto mark as read fix and del keyboard accelerator. 2025-10-28 16:47:06 +01:00
Burak Kaan Köse d02205fba3 Item vm prop changes. 2025-10-28 14:43:22 +01:00
Burak Kaan Köse c461a4daed Swipe action implementations. 2025-10-27 23:22:55 +01:00
Burak Kaan Köse 4f85fa6ba9 New list view items. 2025-10-27 22:52:26 +01:00
Burak Kaan Köse 4eea21c4f5 Better prop change cleanup. 2025-10-27 12:53:15 +01:00
Burak Kaan Köse 7816400c01 Remove collapsing animation on expander. 2025-10-27 12:52:56 +01:00
Burak Kaan Köse 103841c364 More interactions with threads. 2025-10-27 01:43:36 +01:00
Burak Kaan Köse 54ac07f6fb Container cleanups. 2025-10-27 01:00:38 +01:00
Burak Kaan Köse d9fc365aeb Intercepting containers for threads. 2025-10-26 23:35:09 +01:00
Burak Kaan Köse 79d5b6ed40 New WinoListView implementation with multiple selections. 2025-10-26 14:53:22 +01:00
Burak Kaan Köse d4c8ae6cb7 Attempt to bring back ListView. 2025-10-25 10:54:38 +02:00
Burak Kaan Köse 6c37c9e786 Misc fixes. 2025-10-25 10:22:35 +02:00
Burak Kaan Köse ff1c3dece3 Some items view improvements for keyboards accelerators. 2025-10-22 03:45:38 +02:00
Burak Kaan Köse 449c1d3f4d Fixing some issues with ItemsView and selections. 2025-10-21 22:08:56 +02:00
Burak Kaan Köse ae7d576967 Fixing system tray icon. 2025-10-21 15:40:19 +02:00
Burak Kaan Köse 3b3c878d0e Fix resetting selected item on loading more. 2025-10-21 01:57:08 +02:00
Burak Kaan Köse 057edb5488 Custom print dialog and better message registrations 2025-10-21 01:27:29 +02:00
Burak Kaan Köse 4191b7314f Custom print dialog. 2025-10-20 21:10:29 +02:00
Burak Kaan Köse baf4141773 PrintUI thing. 2025-10-20 21:10:14 +02:00
Burak Kaan Köse 7a7281f2fa Remove codepages since it'll be pruned. 2025-10-20 19:18:42 +02:00
Burak Kaan Köse 8e16908fce Fix flickering on item selection and context menus. 2025-10-20 19:17:52 +02:00
Burak Kaan Köse 5e0a0456c4 Some dispatcher fixes. 2025-10-20 18:27:09 +02:00
Burak Kaan Köse fb56001a52 Minimum download logic. 2025-10-20 18:27:02 +02:00
Burak Kaan Köse ecff97419b Default theme is back. Container selection functionality etc. 2025-10-18 22:16:28 +02:00
Burak Kaan Köse ad135c5e32 Bunch of changes for ItemsView and threads. 2025-10-18 11:45:10 +02:00
Cas Cornelissen 89f4b4c05d Fix typo in reorder accounts dialog (#754) 2025-10-17 20:35:53 +02:00
Konstantin Shkel 70a1f1325f fix: QRESYNC initial modseq should be 1 (#734) 2025-10-17 20:35:26 +02:00
Burak Kaan Köse 522a2da114 ItemsView thing. 2025-10-12 16:25:15 +02:00
Burak Kaan Köse 7ca6a65559 Outlook sync improvements. 2025-10-12 16:23:33 +02:00
Burak Kaan Köse 309e891594 Outlook auth fix and actually syncing. 2025-10-06 17:46:00 +02:00
Burak Kaan Köse 9623c2e6d2 Synchronization manager. 2025-10-04 23:10:07 +02:00
Burak Kaan Köse 3b1eff1702 Tray icon implementation. 2025-10-04 15:46:05 +02:00
Burak Kaan Köse a00ff3df46 Fix settings navigation and fix flicker on personalizaton nav. 2025-10-04 14:44:51 +02:00
Burak Kaan Köse 2f5d4dad9a Shell title bar buttons adjustments. 2025-10-04 13:40:35 +02:00
Burak Kaan Köse 20ee4c3567 title bar shell content 2025-10-03 22:12:27 +02:00
Burak Kaan Köse accffe8ef6 Remove connection manager. 2025-10-03 21:55:23 +02:00
Burak Kaan Köse e42ebb49ae Remove old theme service completely. 2025-10-03 21:17:41 +02:00
Burak Kaan Köse 1c49b69332 Couple aot fixes. 2025-10-03 21:13:26 +02:00
Burak Kaan Köse 229006c51d New theme service that supports window backdrop. 2025-10-03 21:04:23 +02:00
Burak Kaan Köse 15b6f5f6fb Some border adjustments 2025-10-03 15:59:37 +02:00
Burak Kaan Köse ec7ac44b87 fix nre on updating thumbnails 2025-10-03 15:49:44 +02:00
Burak Kaan Köse 7b41f558d4 Stub 2025-10-03 15:46:38 +02:00
Burak Kaan Köse 2bec513d2c Some shell/ themes improvements 2025-09-29 19:09:48 +02:00
Burak Kaan Köse f6bf080c9e Remove unused projects. 2025-09-29 11:24:13 +02:00
Burak Kaan Köse 734a3d75db Replace Core.UWP namespace with Core.WinUI 2025-09-29 11:23:44 +02:00
Burak Kaan Köse e67b893ae4 Initial WinUI switch. 2025-09-29 11:16:14 +02:00
Burak Kaan Köse f9c53ca2c9 New Crowdin updates (#724)
* New translations resources.json (Romanian)

* New translations resources.json (French)

* New translations resources.json (Spanish)

* New translations resources.json (Bulgarian)

* New translations resources.json (Catalan)

* New translations resources.json (Czech)

* New translations resources.json (Danish)

* New translations resources.json (German)

* New translations resources.json (Greek)

* New translations resources.json (Finnish)

* New translations resources.json (Italian)

* New translations resources.json (Japanese)

* New translations resources.json (Lithuanian)

* New translations resources.json (Dutch)

* New translations resources.json (Polish)

* New translations resources.json (Russian)

* New translations resources.json (Slovak)

* New translations resources.json (Turkish)

* New translations resources.json (Ukrainian)

* New translations resources.json (Chinese Simplified)

* New translations resources.json (Galician)

* New translations resources.json (Portuguese, Brazilian)

* New translations resources.json (Indonesian)
2025-07-30 23:43:07 +02:00
Burak Kaan Köse 21f9c7cf6d Deprecation of Application Insights for Sentry.IO (#723)
* Remove Application Insights implementation and implement new Sentry.IO SDK

* Remove test exception.
2025-07-30 23:36:10 +02:00
Maicol Battistini 43283b7218 feat(notification): Remove notification when read externally (#707)
* feat(notification):  Add notification removal feature

Implemented a new method `RemoveNotificationAsync` in the `INotificationBuilder` interface to allow the removal of toast notifications for specific emails identified by a unique ID.

This change enhances the notification management by ensuring that notifications can be cleared when emails are marked as read. The `NotificationBuilder` class has been updated to include logic for removing existing notifications and to use the unique ID as a tag for the toast notifications, facilitating their removal. Additionally, the `AppShellViewModel` has been modified to call this new method when an email is updated and marked as read.

This improvement aims to provide a better user experience by keeping the notification area relevant and up-to-date.

* feat(notification):  Add MailReadStatusChanged event handling

Introduced a new event system for handling email read status changes. This includes the addition of a listener in `NotificationBuilder` that removes notifications when an email is marked as read.

• Added `MailReadStatusChanged` record to represent the event.
• Registered a listener in `NotificationBuilder` to handle notification removal.
• Removed the `OnMailUpdated` method from `AppShellViewModel`, delegating notification management to the new event system.
• Updated `MailService` to send `MailReadStatusChanged` events when emails are marked as read.

This change improves the communication between components and enhances the notification management system.

* refactor: Remove comments

* Little cleanup.

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2025-07-26 12:51:53 +02:00
Maicol Battistini c2bb07ff3d feat(preferences): Add email sync interval setting (#710)
* feat(preferences):  Add email sync interval setting

Introduced a new property `EmailSyncIntervalMinutes` in the `IPreferencesService` interface to allow users to configure the email synchronization interval in minutes. This feature enhances user control over email sync behavior.

• Updated `resources.json` to include translations for the new setting.
• Implemented the logic for the new property in `PreferencesService.cs`, with a default value of 3 minutes.
• Added binding and UI support in `AppPreferencesPageViewModel.cs` and `AppPreferencesPage.xaml` to allow users to modify the sync interval.
• Integrated the new setting into `ServerContext.cs` to dynamically adjust the synchronization timer based on user preferences.

This change improves the user experience by providing customizable email synchronization settings.

* Minimum interval and added an icon.

* Proper SetProperty usage.

* Making sure the minimum sync interval is 1 in the ServerContext.

* Making sure the minimum is applied to first trigger of the sync timer.

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2025-07-24 09:45:35 +02:00
Aleh Khantsevich 8cd7f68c30 fix save imap settings and progress ring. (#704)
Added notification that settings saved.
2025-07-07 19:28:56 +02:00
Aleh Khantsevich 3e889d8c08 Make height of single account navigation item smaller (#702)
* Make height of navigation item 50

* fix subtle and heights

* move spacing and margins

* make 52

* fix wrong heights
2025-07-02 23:41:41 +02:00
Aleh Khantsevich a01395aed3 fix tab navigation for compose page (#695) 2025-06-21 13:35:42 +02:00
Aleh Khantsevich 7b3459abff Text input should update property on each changem instead of lost focus (#694) 2025-06-21 01:45:21 +02:00
Aleh Khantsevich 9a88f798fc fix animations (#689) 2025-06-21 01:40:45 +02:00
Maicol Battistini 256fd1cce2 feat: Enhanced sender avatars with gravatar and favicons integration (#685)
* feat: Enhanced sender avatars with gravatar and favicons integration

* chore: Remove unused known companies thumbnails

* feat(thumbnail): add IThumbnailService and refactor usage

- Introduced a new interface `IThumbnailService` for handling thumbnail-related functionalities.
- Registered `IThumbnailService` with its implementation `ThumbnailService` in the service container.
- Updated `NotificationBuilder` to use an instance of `IThumbnailService` instead of static methods.
- Refactored `ThumbnailService` from a static class to a regular class with instance methods and variables.
- Modified `ImagePreviewControl` to utilize the new `IThumbnailService` instance.
- Completed integration of `IThumbnailService` in the application by registering it in `App.xaml.cs`.

* style: Show favicons as squares

- Changed `hintCrop` in `NotificationBuilder` to `None` for app logo display.
- Added `FaviconSquircle`, `FaviconImage`, and `isFavicon` to `ImagePreviewControl` for favicon handling.
- Updated `UpdateInformation` method to manage favicon visibility.
- Introduced `GetBitmapImageAsync` for converting Base64 to Bitmap images.
- Enhanced XAML to include `FaviconSquircle` for improved UI appearance.

* refactor thumbnail service

* Removed old code and added clear method

* added prefetch function

* Change key from host to email

* Remove redundant code

* Test event

* Fixed an issue with the thumbnail updated event.

* Fix cutted favicons

* exclude some domain from favicons

* add yandex.ru

* fix buttons in settings

* remove prefetch method

* Added thumbnails propagation to mailRenderingPage

* Revert MailItemViewModel to object

* Remove redundant code

* spaces

* await load parameter added

* fix spaces

* fix case sensativity for mail list thumbnails

* change duckdns to google

* Some cleanup.

---------

Co-authored-by: Aleh Khantsevich <aleh.khantsevich@gmail.com>
Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2025-06-21 01:40:25 +02:00
Burak Kaan Köse a8cb332232 Type fix. 2025-06-20 14:34:37 +02:00
Victor 89ea2b23a2 Replaced "Dismiss" button in notification popup with "Archive" button (#664)
* replaced "Dismiss" button in notification popup with "Archive" button

fixes https://github.com/bkaankose/Wino-Mail/issues/40

* Fixed incorrect build action for the archive icon.

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2025-06-15 15:27:39 +02:00
Aleh Khantsevich 9b214a66c8 Added new option to hide action labels in mail rendering page (#683)
* Added option to disable labels for mail actions

* Updated spacings and section title styles in settings

* Added translations
2025-06-15 15:17:57 +02:00
Aleh Khantsevich 4c4689ec8d Flyout styles and settings animations (#682)
* Refactor and enhance settings pages and solution structure

- Added transition effects to multiple pages for enhanced UI animations.
- Moved `AboutPage` and `PersonalizationPage` to settings folder.
- Put version into settings card instead of text.

* Fixed main logo in about page and changed version styles

* revert platforms

* Remove useless imprt

* Apply this animation globally

* Added resize transition for mail rendering page

* remove entrance transition from rendering page
2025-06-15 14:54:03 +02:00
Burak Kaan Köse c4e561dee6 dotnet format refactorings. 2025-05-18 14:06:25 +02:00
Burak Kaan Köse 69bfe5b750 Fix calendar server startup. 2025-05-03 20:21:06 +02:00
Burak Kaan Köse 137b3dc2ea Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2025-05-03 19:08:36 +02:00
Burak Kaan Köse ea5f879181 Fixed calendar slnx build. 2025-05-03 19:08:29 +02:00
Burak Kaan Köse 25d5f34f68 Version bump 2025-05-03 19:08:22 +02:00
Dinuru Seniya c8a6df77ac Outlook Auth Fix (#653)
Issue: Account selector dialog pops up endlessly for Outlook/Live accounts. (Stored account not being correctly identified)

Fix: Ignore case differences, add null safety and remove whitespaces when retrieving stored accounts.
2025-05-02 12:12:45 +02:00
Burak Kaan Köse 7b6ac46b6a More informational message for different UPN and address for Outlook authenticator. 2025-04-26 12:25:34 +02:00
Burak Kaan Köse d77c648d54 New Crowdin updates (#646)
* New translations resources.json (Romanian)

* New translations resources.json (French)

* New translations resources.json (Spanish)

* New translations resources.json (Bulgarian)

* New translations resources.json (Czech)

* New translations resources.json (German)

* New translations resources.json (Greek)

* New translations resources.json (Italian)

* New translations resources.json (Dutch)

* New translations resources.json (Polish)

* New translations resources.json (Slovak)

* New translations resources.json (Ukrainian)

* New translations resources.json (Chinese Simplified)

* New translations resources.json (Portuguese, Brazilian)
2025-04-26 11:04:03 +02:00
Burak Kaan Köse c3f47c5fa1 Check account notification preferences after the synchronization. (#647) 2025-04-26 11:02:41 +02:00
Burak Kaan Köse f37a51b46f Remove test code. 2025-04-26 10:51:14 +02:00
Burak Kaan Köse 9feb3f35c3 Synchronizer error factory implementation (#645)
* Added sync error factories for outlook and gmail.

* Implement ObjectCannotBeDeletedHandler for OutlookSynchronizer.

* Remove debug code.

* Implement del key to delete on mail list.

* Revert debug code.
2025-04-26 10:49:55 +02:00
Burak Kaan Köse 5b44cf03ce Don't report when printing is canceled. 2025-04-21 10:31:23 +02:00
Burak Kaan Köse 86a6382463 Max 1500 mails to download per-folder on initial sync for Gmail. 2025-04-21 10:15:42 +02:00
Burak Kaan Köse df991a3829 Bump nugets. 2025-04-21 10:15:05 +02:00
Grigory f243c86b50 build(nuget.config): correct nuget packageSources key name (#623) 2025-04-06 11:33:30 +02:00
Grigory b77be0a5e9 build(Wino.Server.csproj): specify RuntimeIdentifiers (#621) 2025-04-06 11:33:08 +02:00
Burak Kaan Köse 83be587c1a Make sure there are no duplicate items for providers except Gmail when creating mails. 2025-04-04 23:55:50 +02:00
Burak Kaan Köse c6048aea80 Make sure the requests are reflected to UI during synchronization. 2025-03-19 23:37:50 +01:00
Burak Kaan Köse 13b495b0f6 Fixed the Gmail sync identifier update issue and removed the batch message download. 2025-03-19 23:22:57 +01:00
Burak Kaan Köse ac64c35efa Fix for another sequence contains error. 2025-03-19 22:15:28 +01:00
Burak Kaan Köse 127b58601f Remove missing isuread property. 2025-03-18 00:12:31 +01:00
Burak Kaan Köse 1f795b45e9 More visible unread items. 2025-03-18 00:10:45 +01:00
Burak Kaan Köse d26e35ee9a Ctrl + A to select all mails. 2025-03-15 17:43:57 +01:00
Burak Kaan Köse 70e69e9dac Wino Calendar slnx 2025-03-15 15:23:26 +01:00
Burak Kaan Köse 3d88f4212d Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2025-03-15 15:22:43 +01:00
Burak Kaan Köse ad90a9c8f3 Fix: Sequence contains no elements while downloading Gmail messages. 2025-03-15 15:22:01 +01:00
Aleh Khantsevich b43176764b Trim all whitespaces, including \t for unsubscribe links (#599) 2025-03-06 22:34:05 +01:00
Burak Kaan Köse 77f24282e0 Fix incorrect visibility. 2025-03-01 19:43:32 +01:00
Burak Kaan Köse 533f1f1102 1.10.2 release notes. 2025-03-01 19:43:21 +01:00
Burak Kaan Köse 92c5d8bd44 New translations resources.json (Turkish) (#595) 2025-03-01 17:09:54 +01:00
Burak Kaan Köse d754ecb486 New Crowdin updates (#594)
* New translations resources.json (Romanian)

* New translations resources.json (French)

* New translations resources.json (Spanish)

* New translations resources.json (Catalan)

* New translations resources.json (Czech)

* New translations resources.json (Danish)

* New translations resources.json (German)

* New translations resources.json (Greek)

* New translations resources.json (Finnish)

* New translations resources.json (Italian)

* New translations resources.json (Japanese)

* New translations resources.json (Dutch)

* New translations resources.json (Polish)

* New translations resources.json (Russian)

* New translations resources.json (Turkish)

* New translations resources.json (Ukrainian)

* New translations resources.json (Chinese Simplified)

* New translations resources.json (Galician)

* New translations resources.json (Portuguese, Brazilian)

* New translations resources.json (Indonesian)

* New translations resources.json (Lithuanian)
2025-03-01 17:05:04 +01:00
Burak Kaan Köse b18987a95c Added ability to edit imap server configuration. (#593) 2025-03-01 16:53:05 +01:00
EzraWard 0daec61f31 Display app name on Win10 start tiles (#591) 2025-03-01 01:17:42 +01:00
Burak Kaan Köse 8ecf301eb8 Account colors + edit account details. (#592)
* Remove account rename dialog. Implement edit account details page.

* Remove unused folder definition.

* Adressing theming issues and adding reset button. Changing the UI a bit.

* Enable auto indent in initializer. Use service from the application.

* Adding color picker to acc setup dialog. Changing UI of edit acc details page.
2025-03-01 01:17:04 +01:00
Burak Kaan Köse 6080646e89 Don't crash on contact inserts. 2025-02-28 18:21:31 +01:00
Burak Kaan Köse 970a521b66 Pre-warmup on imap synchronizer interface. 2025-02-26 23:13:17 +01:00
Burak Kaan Köse 9b5a92f942 Changing delete logic. 2025-02-26 23:13:05 +01:00
Burak Kaan Köse c4e0f13d67 Pre warmup trigger on synchronizer creation for imaps. 2025-02-26 23:12:01 +01:00
Burak Kaan Köse b6821746d0 Locked busy scope to handle disconnections properly. 2025-02-26 23:11:49 +01:00
Burak Kaan Köse b98fc91a99 Refactoring ImapClientPool. Implemented no-op timer and pre-warmup clients logic. Disabled protocol log per-account. 2025-02-26 23:11:16 +01:00
Burak Kaan Köse bd7f7b867e Making sure missing draft folder is handling during draft creation. 2025-02-26 23:10:30 +01:00
Burak Kaan Köse 32a3fea8d7 Automatically append sent messages to sent folder for iCloud and Yahoo. 2025-02-26 22:57:08 +01:00
Burak Kaan Köse 3561beab1d Revert bump graph. 2025-02-26 22:18:25 +01:00
Burak Kaan Köse 1d1fd52cae Refactoring mail collection class. 2025-02-26 19:59:20 +01:00
Burak Kaan Köse c4ba438150 Handling of generalException and some refactorings on batch executions. 2025-02-26 19:59:11 +01:00
Burak Kaan Köse 37f0ee08b1 Bump graph API. 2025-02-26 19:22:43 +01:00
Burak Kaan Köse 240b02c94e Fix gmail mail service not enabled error. 2025-02-26 19:04:38 +01:00
Burak Kaan Köse e8142ff3df Download messages in ascending order. 2025-02-26 11:45:23 +01:00
Aleh Khantsevich 832b363da7 Improved outlook online search even more and removed redundant methods from ChangeProcessor (#586) 2025-02-24 18:53:11 +01:00
Dinuru Seniya cf8f1ecd67 Code cleanup (#585)
1.  Moved the IsBackground property assignment into the object initializer for the Thread object.

2. Replaced e.Args[e.Args.Length - 1] with e.Args[^1]

3. Added a conditional check to see if GetWindowThreadProcessId returns 0, which indicates failure. If it fails, throw a Win32Exception with the last Win32 error.

4. Removed unused assignment to the variable process

5. Changed the return type of the ConfigureServices method from IServiceProvider to ServiceProvider. It is more specific and faster.

6. Changed notifyIcon to _notifyIcon according to private var naming scheme.

7. Added the CharSet = CharSet.Unicode attribute to the DllImport declarations to specify that the string arguments should be marshaled as Unicode.
2025-02-24 09:50:44 +01:00
Burak Kaan Köse ee5129830c Gmail crash fix. 2025-02-24 09:48:07 +01:00
Aleh Khantsevich 9facfaffa8 Improved online search performance when doing local operations (#584)
* Improved online search performance when doing local operations

* Retruning an empty list on no item searches.

* Fixed an issue with batch imap downloads.

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2025-02-23 22:17:40 +01:00
Burak Kaan Köse 31b859ba1a Release notes for v1.10.2 2025-02-23 20:58:33 +01:00
Burak Kaan Köse b0f5a24c30 New Crowdin updates (#583)
* New translations resources.json (Romanian)

* New translations resources.json (French)

* New translations resources.json (Spanish)

* New translations resources.json (Catalan)

* New translations resources.json (Czech)

* New translations resources.json (Danish)

* New translations resources.json (German)

* New translations resources.json (Greek)

* New translations resources.json (Finnish)

* New translations resources.json (Italian)

* New translations resources.json (Japanese)

* New translations resources.json (Dutch)

* New translations resources.json (Polish)

* New translations resources.json (Russian)

* New translations resources.json (Turkish)

* New translations resources.json (Ukrainian)

* New translations resources.json (Chinese Simplified)

* New translations resources.json (Galician)

* New translations resources.json (Portuguese, Brazilian)

* New translations resources.json (Indonesian)

* New translations resources.json (Lithuanian)
2025-02-23 19:09:27 +01:00
Burak Kaan Köse b60b594e44 Id -> ID in ENG translations. 2025-02-23 19:08:01 +01:00
Burak Kaan Köse a8cee1016b Enable default accounts synchronization and timer sync for debug builds but not if it is attached. 2025-02-23 17:24:59 +01:00
Burak Kaan Köse b551af01fa Missing archive id check for gmail synchronizer. 2025-02-23 17:16:53 +01:00
Burak Kaan Köse b178869a8e Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2025-02-23 17:05:53 +01:00
Burak Kaan Köse 8e1c60d5f0 Gmail - Archive/Unarchive (#582)
* Disable timer back sync for debug builds.

* Archive / unarchive feature for Gmail.

* Archive folder name override for Gmail.

* Possible crash fix when the next item is being selected after a mail is removed.

* Restore proper account selection after pin/unpin of folder.

* Making sure that incorrect arcive folder id is not saved in Gmailsynchronizer due to migration.
2025-02-23 17:05:46 +01:00
Burak Kaan Köse 71ea49439e Disable timer back sync for debug builds. 2025-02-23 16:01:51 +01:00
Burak Kaan Köse 9d0a2f6535 Ignore folder filter if label specific query is passed to Gmail. 2025-02-23 10:21:58 +01:00
Burak Kaan Köse c091fffe90 Hnadling of folder delta token 410 GONE for Outlook. 2025-02-23 00:35:13 +01:00
Burak Kaan Köse 7e05d05f94 Implemented cache reset for Gmail history id expiration. (#581) 2025-02-22 23:09:53 +01:00
Burak Kaan Köse bd5b51c62f Added capability to detect disabled gmail service for Google Workspace accounts during account creation. (#580) 2025-02-22 17:51:38 +01:00
Burak Kaan Köse 1d5eb2eced Added simple validations for advanced imap setup dialog to prevent users from making mistakes. (#579) 2025-02-22 01:54:52 +01:00
Aleh Khantsevich 5073ead8fe Extract webvieweditor to share between compose page and signature editor (#578)
* initial work for webview editor control

* moved more stuff to editor itself

* revert packages.props indention changes

* move alignment logic

* Migrate signature editor to new control

* move background to editor control

* Some polishing

* Fixed the corner glitch issue with dark theme.

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2025-02-22 00:43:39 +01:00
Burak Kaan Köse f61bcb621b Online Search (#576)
* Very basic online search for gmail.

* Server side of handling offline search and listing part in listing page.

* Default search mode implementation and search UI improvements.

* Online search for Outlook.

* Very basic online search for gmail.

* Server side of handling offline search and listing part in listing page.

* Default search mode implementation and search UI improvements.

* Online search for Outlook.

* Online search for imap without downloading the messages yet. TODO

* Completing imap search.
2025-02-22 00:22:00 +01:00
Burak Kaan Köse 42b695854b Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2025-02-20 00:54:46 +01:00
Burak Kaan Köse 496ae8b1b2 Download imap messages in ascending order. 2025-02-20 00:54:41 +01:00
Aleh Khantsevich 4215a2592f Remove last simicolon in to/cc/bcc (#574) 2025-02-18 20:51:02 +01:00
Sean Chen bca62033a1 Log unexpected exceptions on sync failure (#569) 2025-02-16 21:15:31 +01:00
Burak Kaan Köse 18a91f9223 Fix condstore synchronization. 2025-02-16 20:40:53 +01:00
Burak Kaan Köse 474d7c7a26 New Crowdin updates (#568)
* New translations resources.json (Romanian)

* New translations resources.json (French)

* New translations resources.json (Spanish)

* New translations resources.json (Catalan)

* New translations resources.json (Czech)

* New translations resources.json (Danish)

* New translations resources.json (German)

* New translations resources.json (Greek)

* New translations resources.json (Finnish)

* New translations resources.json (Italian)

* New translations resources.json (Japanese)

* New translations resources.json (Dutch)

* New translations resources.json (Polish)

* New translations resources.json (Russian)

* New translations resources.json (Turkish)

* New translations resources.json (Ukrainian)

* New translations resources.json (Chinese Simplified)

* New translations resources.json (Galician)

* New translations resources.json (Portuguese, Brazilian)

* New translations resources.json (Indonesian)

* New translations resources.json (Lithuanian)
2025-02-16 18:13:32 +01:00
Burak Kaan Köse 3f9a51ff46 Fix portuguese - brazil typo. 2025-02-16 17:06:07 +01:00
Burak Kaan Köse df3b5c41f9 Clicking on loaded account menu item will automatically go to Inbox. 2025-02-16 16:56:59 +01:00
Burak Kaan Köse 8800d11ab0 Lower the amount of text needed to start auto-complete in composer page to 2. 2025-02-16 16:56:42 +01:00
Burak Kaan Köse f021834ceb Fixing diagnostic id being not saved properly. 2025-02-16 16:42:48 +01:00
Burak Kaan Köse f54a39a549 Fix missing ; for 'you' 2025-02-16 16:33:02 +01:00
Burak Kaan Köse c312ff3faf Ignore folders that can't be opened for IMAP. 2025-02-16 16:17:41 +01:00
Burak Kaan Köse db833594f4 Make sure idle disconnects are not logged to app insights. 2025-02-16 16:14:50 +01:00
Burak Kaan Köse d36cf59829 Translated dates based on display language. (#567)
* Updating the app's culture based on the display language and making sure that dates/times are properly translated.
2025-02-16 14:46:34 +01:00
Aleh Khantsevich caae751698 Show "You" for active account in mail rendering page (#566)
* Added account contact view model to handle "You" case.

* fix namespaces again
2025-02-16 14:38:53 +01:00
Burak Kaan Köse f7836eedce Tracking failed imap setup steps for app insights. 2025-02-16 13:23:45 +01:00
Aleh Khantsevich 3ddc1a6229 file scoped namespaces (#565) 2025-02-16 11:54:23 +01:00
Burak Kaan Köse cf9869b71e Revert "File scoped namespaces"
This reverts commit d31d8f574e.
2025-02-16 11:43:30 +01:00
Aleh Khantsevich d31d8f574e File scoped namespaces 2025-02-16 11:35:43 +01:00
Burak Kaan Köse c1336428dc AppCenter to AppInsights migration. (#562)
* Remove AppCenter usage and libraries.

* Remove redundant pacakges and add the app insights sink.

* Diagnostic id support and manipulating telemetries.

* Handling of appdomain unhandled exceptions.

* Remove unused package identity package from mail project.

* Fixing printing.
2025-02-16 01:44:41 +01:00
Burak Kaan Köse f0e513bf0d New Crowdin updates (#559)
* New translations resources.json (Romanian)

* New translations resources.json (French)

* New translations resources.json (Spanish)

* New translations resources.json (Catalan)

* New translations resources.json (Czech)

* New translations resources.json (Danish)

* New translations resources.json (German)

* New translations resources.json (Greek)

* New translations resources.json (Finnish)

* New translations resources.json (Italian)

* New translations resources.json (Japanese)

* New translations resources.json (Dutch)

* New translations resources.json (Polish)

* New translations resources.json (Russian)

* New translations resources.json (Turkish)

* New translations resources.json (Ukrainian)

* New translations resources.json (Chinese Simplified)

* New translations resources.json (Galician)

* New translations resources.json (Portuguese, Brazilian)

* New translations resources.json (Indonesian)
2025-02-15 12:55:45 +01:00
Burak Kaan Köse ee9e41c5a7 IMAP Improvements (#558)
* Fixing an issue where scrollviewer overrides a part of template in mail list. Adjusted zoomed out header grid's corner radius.

* IDLE implementation, imap synchronization strategies basics and condstore synchronization.

* Adding iCloud and Yahoo as special IMAP handling scenario.

* iCloud special imap handling.

* Support for killing synchronizers.

* Update privacy policy url.

* Batching condstore downloads into 50, using SORT extension for searches if supported.

* Bumping some nugets. More on the imap synchronizers.

* Delegating idle synchronizations to server to post-sync operations.

* Update mailkit to resolve qresync bug with iCloud.

* Fixing remote highest mode seq checks for qresync and condstore synchronizers.

* Yahoo custom settings.

* Bump google sdk package.

* Fixing the build issue....

* NRE on canceled token accounts during setup.

* Server crash handlers.

* Remove ARM32. Upgrade server to .NET 9.

* Fix icons for yahoo and apple.

* Fixed an issue where disabled folders causing an exception on forced sync.

* Remove smtp encoding constraint.

* Remove commented code.

* Fixing merge conflict

* Addressing double registrations for mailkit remote folder events in synchronizers.

* Making sure idle canceled result is not reported.

* Fixing custom imap server dialog opening.

* Fixing the issue with account creation making the previously selected account as selected as well.

* Fixing app close behavior and logging app close.
2025-02-15 12:53:32 +01:00
Aleh Khantsevich 30f1257983 Attempt to fix source generator issues (#556)
* Changed some properties of source generator project

* Remove accelerate
2025-02-14 19:16:54 +01:00
Aleh Khantsevich f007cef208 Updated Privacy policy URL (#557) 2025-02-14 19:15:42 +01:00
Burak Kaan Köse 19b5852098 Missing package description and fixing typo. 2025-02-14 02:14:04 +01:00
Aleh Khantsevich 2ec05ea7cc UWP .NET9 (#555)
* Ground work for NET9 UWP switch.

* Add launch settings for Wino.Mail

* Added new test WAP project

* fix platforms in slnx solution

* ManagePackageVersionsCentrally set default

* Fixing assets and couple issues with the new packaging project.

* Add back markdown

* Fix nuget warnings

* FIx error in WAP about build tools

* Add build.props with default language preview

* Some AOT compilation progress.

* More AOT stuff.

* Remove deprecated protocol auth activation handler.

* Fix remaining protocol handler for google auth.

* Even more AOT

* More more AOT fixes

* Fix a few more AOT warnings

* Fix signature editor AOT

* Fix composer and renderer AOT JSON

* Outlook Sync AOT

* Fixing bundle generation and package signing.

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2025-02-14 01:43:52 +01:00
Aleh Khantsevich e8dd8bff44 Added save of drafts when closing app (#546) 2025-02-09 10:42:51 +01:00
Aleh Khantsevich ab3f65edfa clear selection on htmlRender (#544) 2025-02-04 21:47:49 +01:00
Aleh Khantsevich fcaf62ecf7 View message source (#541)
* Added view message source

* Change condition to when button available
2025-02-01 18:13:36 +01:00
Burak Kaan Köse 7cfa5a57f5 Event details page navigation, handling of attendees in Outlook synchronizer, navigation changes for calendar. 2025-01-16 22:00:05 +01:00
Burak Kaan Köse 56d6c22d53 Event details page basic layout. 2025-01-14 00:53:54 +01:00
Burak Kaan Köse 10b0b1e96e Revert debug code for background synchronizer. 2025-01-09 22:24:45 +01:00
Burak Kaan Köse 6ef1e9c86c Fix print icon. 2025-01-07 22:23:03 +01:00
Burak Kaan Köse a0d0f2651b Release x64 build properties for Calendar. 2025-01-07 22:13:07 +01:00
Burak Kaan Köse b6edbad744 Fix multi-day event recurrences for monthly calendar. 2025-01-07 22:12:54 +01:00
Sean Chen 05748e23b1 Fix crash on launch when IMAP accounts are present (#507) 2025-01-07 22:10:37 +01:00
Burak Kaan Köse 12d87be106 Speeding up the UI by changing the flip view orientation at correct moment. 2025-01-07 20:51:10 +01:00
Burak Kaan Köse dfb83cc1f7 Sync calendar events for all accounts for testing. 2025-01-07 14:38:45 +01:00
Burak Kaan Köse 1840ae80c2 Fix google calendar sync for bg colors. 2025-01-07 14:38:29 +01:00
Burak Kaan Köse 2e9d1d83a4 Outlook delta synchronization. 2025-01-07 13:42:10 +01:00
Burak Kaan Köse 1ef83a3089 Bump version. 2025-01-07 01:14:05 +01:00
Burak Kaan Köse b95c06a5dc Fix startup launch for Mail. 2025-01-07 01:13:34 +01:00
Burak Kaan Köse 8f789841a6 Fix manage account navigation for Wino Mail 2025-01-07 01:13:25 +01:00
Burak Kaan Köse 0f57a4dfd7 Monthly calendar basics. 2025-01-06 21:56:33 +01:00
Burak Kaan Köse 125c277c88 Outlook calendar/event syncing basics without delta. Bunch of UI updates for the calendar view. 2025-01-06 02:15:21 +01:00
Burak Kaan Köse a7674d436d Handling of multi-day events, new rendering etc. 2025-01-04 11:39:32 +01:00
Burak Kaan Köse 48ba4cdf42 Fixed an issue where some children does not fit in the arranged box. 2025-01-02 22:24:04 +01:00
Burak Kaan Köse 8a9265eb79 Bunch of interaction updates for the calendar item control. 2025-01-02 00:18:34 +01:00
Burak Kaan Köse 215dc6ea6d Basic dragging state for calendar control 2025-01-01 20:19:31 +01:00
Burak Kaan Köse 428dcb2348 New quick event dialog using Popup. 2025-01-01 19:17:54 +01:00
Burak Kaan Köse 1c79d14260 Finalizing quick event dialog. 2025-01-01 17:28:29 +01:00
Burak Kaan Köse a82b487b92 Disable auto embedding of videos pasted on the editor. 2025-01-01 10:49:01 +01:00
Burak Kaan Köse 068369fca2 Some improvements on the quick event tip. 2024-12-31 22:22:19 +01:00
Burak Kaan Köse d524143a53 Some adjustments for all day items control. Removed the leaking part. 2024-12-31 19:21:07 +01:00
Burak Kaan Köse 05ebfa68c9 Idle state loading for calendar. 2024-12-31 15:32:03 +01:00
Burak Kaan Köse 57d8fd7e10 Disable all day items control due to memory leak. 2024-12-31 14:28:37 +01:00
Burak Kaan Köse 64c556a337 Fixing load more issue during flip switches. 2024-12-31 14:28:28 +01:00
Burak Kaan Köse de268d1168 Fixing on calendar event added, and testing method. 2024-12-31 01:30:10 +01:00
Burak Kaan Köse 8fd09bcad4 Proper handling of DateTimeOffset, support for Multi-Day events and reacting to adding/removing events for the days. 2024-12-30 23:10:51 +01:00
Burak Kaan Köse 8cc7d46d7b Updating visibilities on calendar events on calendar activation/deactivation. 2024-12-30 01:15:31 +01:00
Burak Kaan Köse bb56815210 Readable coloring for calendar events. 2024-12-30 01:15:06 +01:00
Burak Kaan Köse 332f2ab89f Removed xaml converter for resolving mail subjects. (#497) 2024-12-29 23:00:42 +01:00
Burak Kaan Köse d3780244cd Improved calendar rendering, standartization of some templates and cleanup of old styles. 2024-12-29 22:30:00 +01:00
Burak Kaan Köse f7bfbd5080 Encapsulation of grouped account selection events and collective events. 2024-12-29 19:37:36 +01:00
Burak Kaan Köse eef2ee1baa Listing account calendars on the shell, some visual state updates and reacting to calendar setting changes properly. 2024-12-29 17:41:54 +01:00
Burak Kaan Köse 8d8d7d0f8c Handling of basic all-day events for calendar. 2024-12-29 13:06:35 +01:00
Burak Kaan Köse 979a3d8f1f Displaying events and all-day events. 2024-12-28 23:17:16 +01:00
Burak Kaan Köse 95b8f54b27 Implementing themes for Calendar. 2024-12-28 20:42:03 +01:00
Burak Kaan Köse 5f1d411b28 Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-12-28 16:39:58 +01:00
Burak Kaan Köse 6e3fcf363a Calendar page and shell improvements to support navigations. Enabled page caching. 2024-12-28 16:39:43 +01:00
Benjin Dubishar 8e129c561d Some miscellaneous cleanup (#494)
* Updating CONTRIBUTING guidelines

* Adding PackageCertificateKeyFile ref to Calendar proj to allow building on fresh machines

* Adding missing conversions to MailSync type
2024-12-27 20:27:45 +01:00
Burak Kaan Köse fbc3ca4517 Synchronizing calendars for gmail and some events. 2024-12-27 00:18:46 +01:00
Burak Kaan Köse 1668dfcce6 More abstraction for mail/calendar. 2024-12-24 18:30:25 +01:00
Burak Kaan Köse da2a58a88b Fix focus-other toggle for Live accounts. 2024-12-22 00:59:59 +01:00
Burak Kaan Köse 8390a868ba Bump release version. 2024-12-22 00:50:01 +01:00
Burak Kaan Köse 296c498464 Fixing serial execution for sending drafts in Outlook. 2024-12-22 00:49:55 +01:00
Kamil a92ff89221 Implement clickable plaintext links and dark mode fix (#488)
* Plaintext links are now clickable and fixes dark mode.

- Change `AppxPackageDir` path from `C:\Users\bkaan\Desktop\Packages\` to `$(USERPROFILE)\Desktop\Packages\`, fixes error when building release.
- Plaintext links are now clickable, and match the same subtle style as Windows Mail.
- Remove `!important` from inline styles to allow Dark Reader to properly recolor the element.

* Implement setting for clickable plaintext link
2024-12-21 20:39:03 +01:00
Burak Kaan Köse e3b2f41a1c Bumping up some nugets. 2024-12-21 20:30:48 +01:00
Burak Kaan Köse 9b3424fa90 Fixing build errors and gmail profile synchronization. 2024-12-21 19:31:24 +01:00
Burak Kaan Köse 678d947f16 Fixing profile sync for gmail and separating authenticators token storage. 2024-12-01 03:05:15 +01:00
Burak Kaan Köse 0cd1568c64 Separation of core library from the UWP app. 2024-11-30 23:05:07 +01:00
Burak Kaan Köse 4e25dbf5e3 Remove redundant extensions folder. 2024-11-30 12:47:46 +01:00
Burak Kaan Köse 14e10c038c Single synchronizer for calendar and mail. 2024-11-30 12:47:24 +01:00
Burak Kaan Köse 2bc5be2105 Compress command for IMAP after the authentication. 2024-11-30 11:48:05 +01:00
Burak Kaan Köse 36b8de470a Fix delete key executing twice on mail list. 2024-11-27 20:43:49 +01:00
Burak Kaan Köse 9abe3dd7b3 Request binding for each request bundle. 2024-11-27 20:32:17 +01:00
Burak Kaan Köse 96c98a6987 New attachment templates that support saving and opening attachment when composing message. 2024-11-27 19:49:10 +01:00
Burak Kaan Köse e586145f50 Fix invalid deserialization during folder rename. 2024-11-27 19:48:50 +01:00
Burak Kaan Köse 611fbfa6df Fix grammer issue with english translation. 2024-11-27 18:40:55 +01:00
Burak Kaan Köse 54eb4f78b2 More abstractions 2024-11-27 03:01:14 +01:00
Burak Kaan Köse 1ce7bb8c02 Seperation of base synchronizer. 2024-11-27 02:51:07 +01:00
Burak Kaan Köse ef17e86465 Fix attachment icons. 2024-11-27 02:30:32 +01:00
Burak Kaan Köse cd3e0492f5 Fix themes and attachment icons. 2024-11-27 01:43:03 +01:00
Burak Kaan Köse c3f98e327c Implement custom wrap panel for reading page. 2024-11-27 01:42:50 +01:00
Burak Kaan Köse 20fc34a6fd Replace progress bar with progress ring for sync progress. 2024-11-26 23:44:32 +01:00
Burak Kaan Köse 87af67c36c Fix for missing attachmnet file icon styles in core library and title bar drag area. 2024-11-26 23:13:11 +01:00
Burak Kaan Köse f33335a768 Missing folder operation changes. 2024-11-26 20:04:25 +01:00
Burak Kaan Köse a9fffd44d2 New change request abstractions. 2024-11-26 20:03:10 +01:00
Burak Kaan Köse e81b7e2e61 Remove base mail integrator. 2024-11-25 17:42:18 +01:00
Burak Kaan Köse 7fad15524f Abstraction of authenticators. Reworked Gmail authentication. 2024-11-20 01:45:48 +01:00
Burak Kaan Köse 8367efa174 Move wino icons font to shared project. 2024-11-11 14:04:22 +01:00
Burak Kaan Köse aa86b7efff Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-11-11 13:57:00 +01:00
Burak Kaan Köse b490450107 Server connectivity for calendar and some abstraction for server. 2024-11-11 13:56:56 +01:00
Xinyang Chen a4a7ff46c5 Fixes mail header encoding issues with MimeKit (#450)
For details see #441
2024-11-11 11:42:19 +01:00
Burak Kaan Köse 418eeb7317 Calendar account management page basics. 2024-11-11 01:09:05 +01:00
Burak Kaan Köse 5b0fcd77e5 Seperation of root account template. 2024-11-11 00:01:42 +01:00
Burak Kaan Köse 757a73ca6b Fix wino mail build error. 2024-11-11 00:01:25 +01:00
Burak Kaan Köse faab29cab7 Fix calendar build error. 2024-11-10 23:37:56 +01:00
Burak Kaan Köse d1d6f12f05 Ground work for Wino Calendar. (#475)
Wino Calendar abstractions.
2024-11-10 23:28:25 +01:00
Burak Kaan Köse a979e8430f Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-11-09 20:48:48 +01:00
Burak Kaan Köse f5f045e8f1 Fix issue when account count is miscalculated for free access. 2024-11-09 20:48:43 +01:00
Burak Kaan Köse 90e291ac8a Allow disabling badge update per-account. (#472) 2024-11-09 20:37:38 +01:00
Burak Kaan Köse b49e1b3a97 Printing mails. (#471)
* Implemented printing functionality.

* Implemented icon for printing.

* Remove debug code.
2024-11-09 19:18:06 +01:00
Burak Kaan Köse 5245feb739 New Crowdin updates (#463)
* New translations resources.json (Romanian)

* New translations resources.json (French)

* New translations resources.json (Spanish)

* New translations resources.json (Catalan)

* New translations resources.json (Czech)

* New translations resources.json (Danish)

* New translations resources.json (German)

* New translations resources.json (Greek)

* New translations resources.json (Finnish)

* New translations resources.json (Italian)

* New translations resources.json (Japanese)

* New translations resources.json (Dutch)

* New translations resources.json (Polish)

* New translations resources.json (Russian)

* New translations resources.json (Turkish)

* New translations resources.json (Ukrainian)

* New translations resources.json (Chinese Simplified)

* New translations resources.json (Galician)

* New translations resources.json (Portuguese, Brazilian)

* New translations resources.json (Indonesian)
2024-11-03 15:54:39 +01:00
Tiktack 550c8fb899 Replace T4 with Source Generator (#459)
* Replace T4 template with source generator

* remove space

* Added summary

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2024-11-03 15:51:08 +01:00
Xinyang Chen 89ffc5d246 Ensure client capabilities are enforced on reconnection (#404)
* Ensure client capabilities are enforced on reconnection

* Cleanup bolierplate
2024-11-03 15:47:33 +01:00
dabardibid 3330873ce0 Update NotificationBuilder.cs (#461)
Mail notifications now use the default Notification Email sound from Windows, just like in the "legacy" Mail app.
2024-11-03 15:46:30 +01:00
Tiktack ff998a8bd1 Fix for To/Cc/Bcc alignment and crush when opening eml (#449)
* Add margin to To/Cc/Bcc

* Fix crash when opening eml

* Remove useless top
2024-11-03 15:45:10 +01:00
Tiktack a1517f82bc Added no subject handler (#451)
* Added no subject handler
changed text trimming for address to CharacterEllipsis

* Make email subject selectable.
2024-11-03 15:44:39 +01:00
Tiktack d8885b089a Removed margin from mail list (#456)
Adjusted margin for group header to match new mail list
2024-11-03 15:44:16 +01:00
Tiktack beba06b8ba Added escaping for line breaks in mailto body to <br> (#419) 2024-10-21 21:03:00 +02:00
Tiktack 762be492bb Added flyout on click for recipients (#436)
User accoutn will be first in recipents list
Recipient shows eather name or address to save space
Added tooltip which shows both ( Name and Address)
Added ";" to have visual separation between names, since we don't have address all the time.
To/Cc/Bcc now at the top of their container, previously they were center and it was hard to understand is it To/CC/Bcc recipient when there many of them
2024-10-21 21:02:02 +02:00
Tiktack c00efff554 Scroll window to the top on render. (#423) 2024-10-21 21:00:32 +02:00
Burak Kaan Köse 5258ae4b34 Release version bump 2024-10-01 19:10:09 +02:00
Burak Kaan Köse a8b19e73fe ImapClientPoolOptions 2024-09-29 21:21:51 +02:00
Burak Kaan Köse 92944c7adc Potential crash for folder sync. 2024-09-29 20:42:03 +02:00
Burak Kaan Köse 0042173ddc Prevent potential crash on threads, somehow... 2024-09-29 13:49:02 +02:00
Burak Kaan Köse a438b5ba17 Fixing regular accounts for mail add check. 2024-09-29 13:37:20 +02:00
Xinyang Chen b86643c052 Fix profile fetch permission issues for Exchange (#401) 2024-09-29 00:45:59 +02:00
Burak Kaan Köse e897182b23 Adding default secure ports to advanced imap setup dialog. 2024-09-27 01:29:56 +02:00
Burak Kaan Köse 56329f02b6 Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-09-27 01:26:19 +02:00
Burak Kaan Köse 11ab579de9 Fixing too many recipients blocking the mail rendering issue. 2024-09-27 01:14:37 +02:00
Tiktack 9aa1de11af Added more handling when fromName is null or empty (#399) 2024-09-27 01:06:57 +02:00
Tiktack 939b395dcd Replace old markdown and remove redundant events (#389)
* Remove redundant messages

* Replaced old markdown with preview.
Added nuget config to add preview feed

* Fix old extensions usage

* Scrollbar margin for the markdown.

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2024-09-27 00:46:51 +02:00
Burak Kaan Köse 118e9bf50b Fixing invalid logic when the e-mail recieved. 2024-09-27 00:13:36 +02:00
Burak Kaan Köse 67828365ca New Crowdin updates (#390)
* New translations resources.json (French)

* New translations resources.json (Spanish)

* New translations resources.json (Catalan)

* New translations resources.json (Czech)

* New translations resources.json (Danish)

* New translations resources.json (German)

* New translations resources.json (Greek)

* New translations resources.json (Finnish)

* New translations resources.json (Italian)

* New translations resources.json (Japanese)

* New translations resources.json (Dutch)

* New translations resources.json (Polish)

* New translations resources.json (Russian)

* New translations resources.json (Turkish)

* New translations resources.json (Ukrainian)

* New translations resources.json (Chinese Simplified)

* New translations resources.json (Galician)

* New translations resources.json (Portuguese, Brazilian)

* New translations resources.json (Indonesian)

* New translations resources.json (Romanian)
2024-09-23 18:37:50 +02:00
Burak Kaan Köse e0d99257fe Skipping unknown tags in mime visitor. 2024-09-19 01:16:42 +02:00
Tiktack e628a98cb8 Fixed visual bug with margin in narrow state (#383)
* Reworked paddings/margins for mail list

* Fix composer page

* Moved layput changes from code behind to visual states
2024-09-19 00:52:27 +02:00
Burak Kaan Köse a4f9284970 Making sure that server is terminated even though there are no accounts. 2024-09-15 00:30:08 +02:00
Burak Kaan Köse c403a716dd Non-message item check for only non-existing messages. 2024-09-14 22:23:12 +02:00
Burak Kaan Köse 4278c8bacb Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-09-14 22:22:53 +02:00
Burak Kaan Köse 56bfbeca58 SSL Handshake Prompt for IMAP (#381)
* Fix an incorrect namespace for copy auth url request.

* Implemented SSL handshake process for testing imap configuration.

* Implemented SSL handshake process for testing imap configuration.

* Replace certificate PathIcon with WinoFontIcon in XAML.
2024-09-14 21:51:43 +02:00
Burak Kaan Köse b6f9eae7b5 Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-09-14 21:17:23 +02:00
Tiktack cad9250cb7 Feature/update icons (#378)
* Added a few icons to ttf font

* Replace junk with blocked

* Fix build

* Remove outdated commandBar styles

* Added Icomoon file to root

* Added imap icon
2024-09-14 21:15:38 +02:00
Tiktack 4ac8095554 Fixes Webview jumps when navigating emails and added Tooltips to the collapsed nav view. (#373)
* Prepare To/Cc/Bcc info in advance to avoid layout shifts

* Changed tooltips in nav view to apply to whole element instead of content

* Revert comment
2024-09-14 18:41:33 +02:00
Burak Kaan Köse 05d16983eb Fix an incorrect namespace for copy auth url request. 2024-09-14 05:08:30 +02:00
Burak Kaan Köse 12f821fd6b Test certificate 2024-09-14 01:42:55 +02:00
Burak Kaan Köse 13c8bf5f19 Prevent crashes on resume due to connectivity issues. 2024-09-14 01:33:35 +02:00
Burak Kaan Köse 9a44e30e0f Ability to copying authorization URL for Gmail (#375)
* Implemented copying auth URL for Gmail authentication.

* Update Button icon and add row spacing in Flyout grid

The icon used in the Button.Content has been updated to a new
design and is now wrapped inside a Viewbox with a width of 20
to ensure proper scaling. Additionally, the Grid inside the
Flyout now includes RowSpacing="12" to improve visual separation
between rows.
2024-09-14 01:17:03 +02:00
Burak Kaan Köse bf77572041 Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-09-13 02:56:34 +02:00
Burak Kaan Köse 18ba4851d1 Release 1.9.2.0. 2024-09-13 02:56:28 +02:00
Burak Kaan Köse 6620034d98 New Crowdin updates (#368)
* New translations resources.json (French)

* New translations resources.json (Spanish)

* New translations resources.json (Catalan)

* New translations resources.json (Czech)

* New translations resources.json (Danish)

* New translations resources.json (German)

* New translations resources.json (Greek)

* New translations resources.json (Finnish)

* New translations resources.json (Italian)

* New translations resources.json (Japanese)

* New translations resources.json (Dutch)

* New translations resources.json (Polish)

* New translations resources.json (Russian)

* New translations resources.json (Turkish)

* New translations resources.json (Ukrainian)

* New translations resources.json (Chinese Simplified)

* New translations resources.json (Galician)

* New translations resources.json (Portuguese, Brazilian)

* New translations resources.json (Indonesian)

* New translations resources.json (Romanian)
2024-09-13 02:54:22 +02:00
Burak Kaan Köse e93ecc7e4a Canceling authentication and Outlook improvements (#367)
* Cancellation support for getting responses from the server.

* Adding cancel button for account creation dialog initialization.

* Prevent invalid outlook message types like contact, calendar event or todo item.

* Remove debug launcher options.
2024-09-13 02:51:37 +02:00
Burak Kaan Köse f85085de41 Add support for custom names for aliases. Synchronization of names for the gmail api. (#365) 2024-09-12 01:14:40 +02:00
Burak Kaan Köse 310943590b Don't crash on Outlook profile picture fetch. 2024-09-12 00:50:49 +02:00
Burak Kaan Köse c1dcd52a28 Don't crash when StartupTask is not present. 2024-09-12 00:08:31 +02:00
Burak Kaan Köse 9bee5e449f Updating unread badge count from server. 2024-09-12 00:02:14 +02:00
Tiktack 02e99066ca Hotfix: Change FontIcon to PathIcon/SymbolIcon for Win10 users. (#358)
* Replaced Segoe Icons with Path/Symbol

* Add tooltips
2024-09-10 17:20:14 +02:00
Tiktack 7761cf6dbe Code Quality: Fixed multiple paddings, disabled backspace addon (#354)
* Changed paddings and size for unsubscribe button to avoid shifts

* Fix bottom paddings

* Disable backspace addon, simplify attachments logic, convert switches

* Remove redundant code from Mail Rendering page

* Replace Font Icon to Path
2024-09-10 10:14:13 +02:00
Burak Kaan Köse 7715d4bdda Fixing an issue where replacing folders with expanded items causing unmanaged crash. 2024-09-05 18:11:50 +02:00
Burak Kaan Köse f3c4906f88 Fixing Outlook attachment issues. 2024-09-05 17:23:15 +02:00
Burak Kaan Köse 3dc16fa07b Fix romanian language. 2024-09-03 15:40:16 +02:00
Burak Kaan Köse be58bdf24f New Crowdin updates (#347)
* New translations resources.json (French)

* New translations resources.json (Spanish)

* New translations resources.json (Catalan)

* New translations resources.json (Czech)

* New translations resources.json (Danish)

* New translations resources.json (German)

* New translations resources.json (Greek)

* New translations resources.json (Finnish)

* New translations resources.json (Italian)

* New translations resources.json (Japanese)

* New translations resources.json (Dutch)

* New translations resources.json (Polish)

* New translations resources.json (Russian)

* New translations resources.json (Turkish)

* New translations resources.json (Ukrainian)

* New translations resources.json (Chinese Simplified)

* New translations resources.json (Galician)

* New translations resources.json (Portuguese, Brazilian)

* New translations resources.json (Indonesian)

* New translations resources.json (Romanian)
2024-09-03 15:38:00 +02:00
Burak Kaan Köse ac170c67bf Enable Turkish and Romanian translations. 2024-09-03 15:35:30 +02:00
Burak Kaan Köse 169cd9d743 Disable tree view re orderings. 2024-09-03 15:34:42 +02:00
Burak Kaan Köse a1931f08a8 Remove building tools for packaging project. 2024-09-03 11:49:01 +02:00
Burak Kaan Köse 5617206c6d Spam filter check per-message. 2024-09-03 11:48:46 +02:00
Burak Kaan Köse cee00b8b2b Store release 1.9.0. 2024-09-03 03:12:52 +02:00
Burak Kaan Köse 323a8acbd5 Fallback to welcome page if startup entity is not found. 2024-09-03 01:49:45 +02:00
Burak Kaan Köse d488b10848 Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-09-03 01:39:57 +02:00
Burak Kaan Köse 24c99364ef Fix incorrect title-message for missing account dialog. 2024-09-03 01:38:52 +02:00
Burak Kaan Köse f8b6975e70 Fixing serialization issue with MimeMessage. 2024-09-02 22:19:26 +02:00
Burak Kaan Köse c416e8c1fb Merge pull request #346 from bkaankose/fix/centering-and-tooltips
Center new Segmented component and add tooltips to action bar
2024-09-01 11:26:16 +02:00
Aleh Khantsevich 51626dfd04 Remove redundand command bar classes 2024-09-01 01:28:36 +02:00
Aleh Khantsevich cb05e58f1e Center Sigmented and add tooltips 2024-09-01 01:12:00 +02:00
Burak Kaan Köse 24c8cfd402 Revert padding for mail list container. 2024-08-31 18:23:22 +02:00
Burak Kaan Köse 8257b0b582 Merge pull request #345 from bkaankose/hotfix/Expander
New expander control for conversation threads
2024-08-31 16:12:32 +02:00
Burak Kaan Köse e612f2c281 Remove redundant code. 2024-08-31 16:11:19 +02:00
Burak Kaan Köse 155df59b1d Increase pre-defined size to fit mail icon changes. 2024-08-31 16:11:11 +02:00
Burak Kaan Köse 6efab9f386 merged main 2024-08-31 15:31:29 +02:00
Burak Kaan Köse 422105a507 Merge pull request #344 from bkaankose/features/action-bar
Mail List action bar improvements
2024-08-31 15:28:59 +02:00
Burak Kaan Köse d58438ab1d Removed margin and increased padding. 2024-08-31 15:26:22 +02:00
Burak Kaan Köse 209aa1a89f 4x top margin for mail list. 2024-08-31 15:22:20 +02:00
Aleh Khantsevich 07ac81583e Fix delete action on KeyPress 2024-08-31 15:11:28 +02:00
Burak Kaan Köse ee6249bb17 Selecting first mail when thread is expanded. 2024-08-31 15:08:43 +02:00
Aleh Khantsevich b8ca3f8604 Disable toolbar when nothing selected. 2024-08-31 14:29:57 +02:00
Aleh Khantsevich 85b5469d96 Fix wrong logic for set/unset flag 2024-08-31 14:19:43 +02:00
Aleh Khantsevich d3ddf7b191 style/renaming 2024-08-31 13:56:31 +02:00
Aleh Khantsevich ebf196ec73 Remove redundant message 2024-08-31 13:39:32 +02:00
Aleh Khantsevich 85c3833452 Make TopBar dynamic 2024-08-31 13:25:55 +02:00
Burak Kaan Köse 8fb4735fc2 Handling hover action tap delegates. 2024-08-31 03:37:04 +02:00
Burak Kaan Köse 6bb09f10d2 Finished wino expander implementation. 2024-08-31 03:18:43 +02:00
Aleh Khantsevich a4ff67e8f4 Fix margins 2024-08-31 00:42:17 +02:00
Burak Kaan Köse b9a1756f90 Moving expander chevron to mail display control. 2024-08-30 02:20:16 +02:00
Burak Kaan Köse 72ff8e67ed Merge branch 'main' into hotfix/Expander 2024-08-30 01:11:19 +02:00
Burak Kaan Köse d7006365eb Merge pull request #342 from bkaankose/hotfix/InvalidContactCrashFix
Fixed invalid contacts causing folder loading to crash.
2024-08-30 01:05:01 +02:00
Burak Kaan Köse 86ef78b296 Fixed invalid contacts causing folder loading to crash. 2024-08-30 01:03:35 +02:00
Aleh Khantsevich 0d84e409c5 Fix button sizes 2024-08-30 00:15:13 +02:00
Burak Kaan Köse 17c7b33167 Merge pull request #341 from bkaankose/hotfix/FixGmailDelete
Fixing Gmail SENT label deletes.
2024-08-30 00:03:53 +02:00
Burak Kaan Köse 856e1613a0 Fixing Gmail sent folder deletes. 2024-08-29 23:58:39 +02:00
Burak Kaan Köse 8db34289a7 Fixing single threads for API threading strategy. 2024-08-29 23:43:49 +02:00
Burak Kaan Köse 3016f70349 Handling gmail errors. 2024-08-29 22:43:27 +02:00
Aleh Khantsevich bdf212fdb3 Remove bottom padding of mail list 2024-08-29 17:36:50 +02:00
Aleh Khantsevich c6216f54f8 simplify layout for mail list 2024-08-29 17:18:46 +02:00
Aleh Khantsevich 945c747e3e Added setting to show/hide action bar in mail list 2024-08-29 01:13:51 +02:00
Aleh Khantsevich 552fca8df7 Moved refresh and multi select buttons 2024-08-29 00:19:24 +02:00
Burak Kaan Köse 4dac160619 Fix 995 char limit on message headers for Outlook. 2024-08-28 22:17:13 +02:00
Burak Kaan Köse fc0e746e1b Reduce outlook send draft delay to 6 secs. 2024-08-27 01:48:03 +02:00
Burak Kaan Köse 8374b5fc0c Fix replying issues with Outlook due to cc or bcc headers. 2024-08-27 01:47:50 +02:00
Burak Kaan Köse 52923ed35b Bump some more nugets, remove redundant events, and fix Outlook profile sync permission issue. 2024-08-26 22:09:00 +02:00
Aleh Khantsevich f002ccfa3a Replace WinoPivot with segmented 2024-08-26 17:27:27 +02:00
Burak Kaan Köse b64cc44531 Bumping some nugets. 2024-08-26 11:30:29 +02:00
Burak Kaan Köse 1b51982551 Merge pull request #335 from bkaankose/code-quality/tabbed-commandbar
Code Quality: TabbedCommandBar
2024-08-26 01:14:29 +02:00
Burak Kaan Köse f4bbf6eb73 New expander control. 2024-08-26 01:07:51 +02:00
Aleh Khantsevich 10c94efa57 Fix priority button size 2024-08-26 00:58:41 +02:00
Aleh Khantsevich c84316e974 Added translations for discard/send buttons in composer 2024-08-26 00:31:12 +02:00
Aleh Khantsevich 7e4d1fbf49 Replace custom pivot + command bar with tabbed commandbar 2024-08-25 23:52:55 +02:00
Burak Kaan Köse 31c7c8b46f Remove unused namespace. 2024-08-25 21:45:13 +02:00
Burak Kaan Köse 8cdb6646c4 Fixed account re-ordering issues with merged accounts. 2024-08-25 19:51:19 +02:00
Burak Kaan Köse 43a51e5f2f Per-session ImapSynchronizer protocol log based on accounts. 2024-08-25 10:32:07 +02:00
Burak Kaan Köse d0b54ea44b Prevent delay between mail addings and better folder init cancellation. 2024-08-25 02:01:08 +02:00
Burak Kaan Köse c8fce82dc1 Fix merged accounts report progress for incorrect account during synchronization. 2024-08-24 21:09:20 +02:00
Burak Kaan Köse 3ffccaa7e5 Fixed non-updating UI for thread items. 2024-08-24 18:12:20 +02:00
Burak Kaan Köse 3f7e7a1474 Generalized sender-from name for IMAP synchronizer. 2024-08-24 17:35:46 +02:00
Burak Kaan Köse d30c15464b Fix account folder layout is not updated if there is a change after the synchronization. 2024-08-24 17:22:47 +02:00
Burak Kaan Köse 2a1f748469 Fix for to,cc and bcc fields are cut out in rendering page. 2024-08-24 17:22:03 +02:00
Burak Kaan Köse 74b429b1bf Applying default xaml styler configs. 2024-08-24 16:16:56 +02:00
Burak Kaan Köse 7afe1b517c Ignore task bar pinning errors. 2024-08-24 16:10:14 +02:00
Burak Kaan Köse fcdcf5692f Fix indentation. 2024-08-24 16:06:36 +02:00
Burak Kaan Köse 735baa67ed Default XamlStyler config for the solution. 2024-08-24 16:06:14 +02:00
Burak Kaan Köse ac00caf83e AccountContact comments. 2024-08-24 16:06:06 +02:00
Burak Kaan Köse 2ccda353e9 Debug log messages on updating flag changes. 2024-08-24 15:54:35 +02:00
Burak Kaan Köse 4257ca54b7 Fix updating folder on name changes for imap. 2024-08-24 15:54:12 +02:00
Burak Kaan Köse 20dd2ef98d Min log level to Debug for debug builds. 2024-08-24 15:53:46 +02:00
Burak Kaan Köse 8be52c9ddd Capitilize first letter of folder names if all capital for IMAP. 2024-08-24 15:38:52 +02:00
Burak Kaan Köse 3bea6619fa Enable all folder sync for gmail. 2024-08-24 15:38:23 +02:00
Burak Kaan Köse 0e5fb11c52 Fix for missing folder update changes for gmail. 2024-08-24 15:26:08 +02:00
Burak Kaan Köse fc47f7701d Always focus to first line on editor focus. 2024-08-24 15:13:02 +02:00
Burak Kaan Köse 3e4ccf8de4 Merge pull request #331 from bkaankose/feature/ContactPictures
Account contact pictures
2024-08-24 12:55:04 +02:00
Burak Kaan Köse 8abb3c709b More logging for App.xaml.cs 2024-08-24 12:54:39 +02:00
Burak Kaan Köse 5263900620 Async cancellation support for contact picture loading. 2024-08-24 01:57:36 +02:00
Burak Kaan Köse 54ee9e5072 Remove redundant IsKnown property. 2024-08-24 01:57:19 +02:00
Burak Kaan Köse 20f4857405 Root contact concept. 2024-08-24 00:14:32 +02:00
Burak Kaan Köse 55110dd39d Merge pull request #333 from bkaankose/l10n_main
New Crowdin updates
2024-08-23 15:04:04 +02:00
Burak Kaan Köse 07d8111df9 New translations resources.json (Indonesian) 2024-08-23 15:03:46 +02:00
Burak Kaan Köse a701b97f1e New translations resources.json (Portuguese, Brazilian) 2024-08-23 15:03:45 +02:00
Burak Kaan Köse b025537d62 New translations resources.json (Galician) 2024-08-23 15:03:44 +02:00
Burak Kaan Köse e68bc2de65 New translations resources.json (Chinese Simplified) 2024-08-23 15:03:43 +02:00
Burak Kaan Köse d0b1c93382 New translations resources.json (Ukrainian) 2024-08-23 15:03:42 +02:00
Burak Kaan Köse a08fa9eabf New translations resources.json (Turkish) 2024-08-23 15:03:40 +02:00
Burak Kaan Köse 65ef130bda New translations resources.json (Russian) 2024-08-23 15:03:39 +02:00
Burak Kaan Köse 32471a71e5 New translations resources.json (Polish) 2024-08-23 15:03:38 +02:00
Burak Kaan Köse ca80f01907 New translations resources.json (Dutch) 2024-08-23 15:03:37 +02:00
Burak Kaan Köse b1fae57922 New translations resources.json (Japanese) 2024-08-23 15:03:36 +02:00
Burak Kaan Köse dea01dda2d New translations resources.json (Italian) 2024-08-23 15:03:35 +02:00
Burak Kaan Köse 9777619259 New translations resources.json (Finnish) 2024-08-23 15:03:34 +02:00
Burak Kaan Köse 6db0f84f8f New translations resources.json (Greek) 2024-08-23 15:03:32 +02:00
Burak Kaan Köse 84e382fcc5 New translations resources.json (German) 2024-08-23 15:03:31 +02:00
Burak Kaan Köse eceed1b934 New translations resources.json (Danish) 2024-08-23 15:03:30 +02:00
Burak Kaan Köse e7b5cd74a4 New translations resources.json (Czech) 2024-08-23 15:03:29 +02:00
Burak Kaan Köse a98930791c New translations resources.json (Catalan) 2024-08-23 15:03:28 +02:00
Burak Kaan Köse 67b0389097 New translations resources.json (Spanish) 2024-08-23 15:03:27 +02:00
Burak Kaan Köse ff30595fb4 New translations resources.json (French) 2024-08-23 15:03:26 +02:00
Burak Kaan Köse d272b62c45 Remove commented code. 2024-08-23 14:54:30 +02:00
Burak Kaan Köse c1973023d0 Fix for webview2 not focusing properly issue. 2024-08-23 03:00:22 +02:00
Burak Kaan Köse ef4689619e Displaying contact picture for rendering page and small adjustments for addresses UI. 2024-08-23 02:23:52 +02:00
Burak Kaan Köse 9ed297a49d New contact picture display for address fields. 2024-08-23 02:23:27 +02:00
Burak Kaan Köse 9950729080 New contact retrival method. 2024-08-23 02:07:50 +02:00
Burak Kaan Köse 36eec9d061 Remove obsolete code. 2024-08-23 02:07:32 +02:00
Burak Kaan Köse fd3a977009 Creating contract on profile update. 2024-08-23 02:07:25 +02:00
Burak Kaan Köse ff88832cca Asynchronously load correct contact data for composer. 2024-08-23 02:06:58 +02:00
Burak Kaan Köse d69b72b77d Fixed not-scrollable address infos for to,cc and bcc fields. 2024-08-23 02:06:28 +02:00
Burak Kaan Köse d9bd9e996b Fix for duplicating incorrect address info in rendering page. 2024-08-23 02:05:15 +02:00
Burak Kaan Köse f45580be70 Adding contact details for loaded mails and fixing background notification actions. 2024-08-23 01:07:00 +02:00
Burak Kaan Köse 0fbeb11304 Merge pull request #329 from bkaankose/feature/NewStartup
Handling app termination and reworked dialog messages.
2024-08-22 18:41:38 +02:00
Burak Kaan Köse 6a70c13b57 Merge pull request #330 from bkaankose/fix/editor-full-height
Make Jodit fill all available height of webview
2024-08-22 14:01:25 +02:00
Aleh Khantsevich f797520e56 Make Jodit fill all available height of webview 2024-08-22 01:59:09 +02:00
Burak Kaan Köse d060db3c96 Fixing an issue where doing folder config on account does not refresh the folder list. 2024-08-22 01:20:08 +02:00
Burak Kaan Köse 298344c2ab Remove async from adding attachments. 2024-08-22 00:57:46 +02:00
Burak Kaan Köse 53dbeadabb Prevent asking users for startup launch if they already did that. 2024-08-22 00:52:41 +02:00
Burak Kaan Köse 93087d7aa7 Ask for enable startup on first launch. 2024-08-22 00:51:10 +02:00
Burak Kaan Köse c304517fc2 Revert debug code. 2024-08-21 23:30:57 +02:00
Burak Kaan Köse af13e034c3 New resource strings for app closing handlers. 2024-08-21 23:22:32 +02:00
Burak Kaan Köse e6b9d59160 Potential crash for imap. 2024-08-21 23:16:46 +02:00
Burak Kaan Köse bd9cbe30c5 Fixed crashing issue with disposing composing page. 2024-08-21 23:14:59 +02:00
Burak Kaan Köse f627226da9 Server termination and refactoring message dialogs. 2024-08-21 22:42:52 +02:00
Burak Kaan Köse bab3272970 Fix couple potential crash issues due to unsupported APIs. 2024-08-21 19:50:14 +02:00
Burak Kaan Köse 003085db7e Merge pull request #328 from bkaankose/hotfix/LogsArchive
Fixing sharing logs
2024-08-21 13:58:39 +02:00
Burak Kaan Köse 8f98bd37c7 Implemented log archive. 2024-08-21 13:54:24 +02:00
Burak Kaan Köse 6971ef1ede Remove imap protocol log sharing since it's already moved to a separate dialog when error occurs during setup. 2024-08-21 13:49:38 +02:00
Burak Kaan Köse 0baac3dc49 Addressing some Outlook sending issues due to API delay. 2024-08-21 13:15:50 +02:00
Burak Kaan Köse 16feb8602d Getting rid of ARM32 and preperation for beta 1.8.3. 2024-08-21 03:54:36 +02:00
Burak Kaan Köse d623129d56 Merge pull request #325 from bkaankose/features/mail-list-splitter
Mail List splitter
2024-08-19 21:21:28 +02:00
Aleh Khantsevich 9cc4c33bb1 Fix multiselect and hide sizer 2024-08-19 21:16:58 +02:00
Burak Kaan Köse c087b40d4a Renaming event handlers and fixing the search box margin issue. 2024-08-19 21:07:21 +02:00
Aleh Khantsevich a82e074bd4 Fix merge conflicts 2024-08-19 20:50:14 +02:00
Burak Kaan Köse 3365c099bb Missing comments. 2024-08-19 20:43:26 +02:00
Aleh Khantsevich d8705de26f Replaced setting with reset button 2024-08-19 20:41:55 +02:00
Burak Kaan Köse 3af181e736 Merge pull request #324 from bkaankose/feature/Aliases
E-mail Aliases Support
2024-08-19 20:37:58 +02:00
Burak Kaan Köse ba6c01b7c6 Missing comments. 2024-08-19 19:16:54 +02:00
Burak Kaan Köse 7a7cdcb041 Remove json.net dependency to STJson 2024-08-19 19:16:47 +02:00
Burak Kaan Köse 09e52bf199 Remove not implemented comment. 2024-08-19 19:05:54 +02:00
Burak Kaan Köse a8c39a1587 Fixing send draft issue with OutlookSynchronizer. 2024-08-19 19:02:33 +02:00
Aleh Khantsevich 68536d6c34 Fix padding in narrow state 2024-08-19 18:49:35 +02:00
Aleh Khantsevich f57c27e755 Fix multiple items selected 2024-08-19 17:15:59 +02:00
Aleh Khantsevich 9a97a27c8a Init 2024-08-19 16:26:15 +02:00
Burak Kaan Köse 07bb90dda9 Refactoring outlook draft creation and sending. 2024-08-19 03:44:16 +02:00
Burak Kaan Köse 3bb156f4da Handling of OutlookSynchronizer alias. 2024-08-18 22:45:23 +02:00
Burak Kaan Köse e13e0efcc6 Bump some nugets. 2024-08-18 22:44:55 +02:00
Burak Kaan Köse 3ae0a94159 Remove folder init progress bar in mail list page. 2024-08-18 22:27:31 +02:00
Burak Kaan Köse eec67ec7dc Fixed an issue where re-loading messages with attachments break the included attachment encodings. 2024-08-18 22:25:29 +02:00
Burak Kaan Köse cf51853eec Removed non-mandatory reply-to validation. 2024-08-18 01:11:23 +02:00
Burak Kaan Köse 67838b28a4 Syntactic sugar. 2024-08-18 01:06:21 +02:00
Burak Kaan Köse bf68e3b7d5 Fix sending draft issue. 2024-08-18 01:05:43 +02:00
Burak Kaan Köse 91ed0bb8bd Ability to select alias in composer page. 2024-08-17 22:55:58 +02:00
Burak Kaan Köse 55fe791c2a Handling of missing mime downloads and mail processed messages. 2024-08-17 20:19:01 +02:00
Burak Kaan Köse 747efac2ec Reworked aliases. 2024-08-17 19:54:52 +02:00
Burak Kaan Köse a87df2e9f6 Fixed an issue where deleting account navigates back to mail list of the next account. 2024-08-17 19:54:44 +02:00
Burak Kaan Köse 2e4a664744 More detailed ImapImplementation for ID extension. 2024-08-17 19:53:50 +02:00
Tiktack 579a22ea45 Remove self from reply all when no other recepients (#319) 2024-08-17 15:00:25 +02:00
Burak Kaan Köse abff850427 Managing account aliases and profile synchronization for outlook and gmail. 2024-08-17 03:43:37 +02:00
Burak Kaan Köse f1154058ba Fix ascending download for messages. QQ server issue will be handled later. 2024-08-17 00:03:45 +02:00
Burak Kaan Köse cf9f308b7f Updating aliases during profile sync for Gmail. 2024-08-16 01:29:31 +02:00
Burak Kaan Köse 1791df236c Remove unused extension class. 2024-08-16 01:03:00 +02:00
Burak Kaan Köse 7211f94f08 Try - catch for outlook profile sync. 2024-08-16 00:40:10 +02:00
Burak Kaan Köse 7b0343c87f Added sender name comment for gmail. 2024-08-16 00:37:50 +02:00
Burak Kaan Köse b80f0276b4 Sender Name and Profile Picture synchronization for Outlook 2024-08-16 00:37:38 +02:00
Burak Kaan Köse 8f66fcbb00 Activated contact service for Gmail to retrieve profile picture and sender name. 2024-08-15 23:57:45 +02:00
Burak Kaan Köse fe449ee1f3 Comments for alias entity. 2024-08-15 16:13:18 +02:00
Burak Kaan Köse 34d6d95186 Including ReplyToAddress for alias. 2024-08-15 16:11:12 +02:00
Burak Kaan Köse 05ddc0660a Creating MailAccountAlias entity. 2024-08-15 16:02:02 +02:00
Burak Kaan Köse c6047a8428 Version bump 2024-08-13 23:40:09 +02:00
Burak Kaan Köse bc4838578e Handling null client connection while sending server response. 2024-08-13 22:57:36 +02:00
Burak Kaan Köse 548996405a Fix incorrect accounts' mails are going to different accounts. 2024-08-13 22:54:36 +02:00
Burak Kaan Köse a9a5f0bd14 Ascending downloading of mails since some servers require it. 2024-08-13 22:54:14 +02:00
Burak Kaan Köse ec05ff6123 Optional splash screen. 2024-08-13 19:26:24 +02:00
Burak Kaan Köse 10c7ab421b Setting exception on connection failure. 2024-08-13 16:14:25 +02:00
Burak Kaan Köse a8a5cc53ea Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-08-13 16:13:25 +02:00
Burak Kaan Köse 8fe48ca438 Fixed an issue where reconnecting doesn't await the handle in the second attempt. 2024-08-13 16:12:34 +02:00
Tiktack cbd5a515a9 Fix account signature preferences during draft creation (#314)
* Pass account ID instead of account to draft creation method, since account object can be stale.

* Configure await
2024-08-12 00:56:26 +02:00
Tiktack 5912adff93 Embedded images replaced with cid linked resources. (#313)
* Added logic to replace embedded images with linked resources

* Added alt text for images and replaced NewtonSoft with Text.Json

* Fix draft mime preparation

* Fix crashes for signatures without images.

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
2024-08-11 23:58:54 +02:00
Burak Kaan Köse 983bc21448 Removing server init from the app init. Making sure server connection is established before doing a request. Handling Connecting state. 2024-08-11 15:25:40 +02:00
Burak Kaan Köse 6d08368462 Hiding reconnect flyout on clicking reconnect. 2024-08-11 15:18:23 +02:00
Burak Kaan Köse cde7bb3524 Merged main. 2024-08-10 14:35:26 +02:00
Burak Kaan Köse 133dc91561 Prevent crashes on invalid Uri for protocol activation. 2024-08-10 14:35:01 +02:00
Tiktack f408f59beb Improve mailto links handling (#310)
* Refactor draft creation

* try scoped namespace

* Refactor mailto protocol and revert namespaces

* Remove useless account query

* Fix typo and CC/BCC in replies

* Replace convert with existing extension

* Small fixes

* Fix CC/Bcc in replies to automatically show if needed.

* Fixed body parameter position from mailto parameters

* Fixed issue with ReplyAll self not removed
2024-08-10 14:33:02 +02:00
Burak Kaan Köse 8763bf11ab Fix typo. 2024-08-09 14:23:51 +02:00
Burak Kaan Köse 99592a52be Unregistration condition. 2024-08-09 02:02:11 +02:00
Burak Kaan Köse 25a8a52573 Remove SessionConnectedTask 2024-08-09 01:51:21 +02:00
Burak Kaan Köse 5901344459 Remove SessionConntectedTask. 2024-08-09 01:24:55 +02:00
1892 changed files with 151657 additions and 48019 deletions
+7 -2
View File
@@ -8,6 +8,9 @@ dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
indent_size = 4
end_of_line = crlf
[XamlTypeInfo.g.cs]
dotnet_diagnostic.CS0612.severity = none
dotnet_diagnostic.CS0618.severity = none
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
@@ -149,7 +152,7 @@ csharp_preferred_modifier_order = public,private,protected,internal,static,exter
# Code-block preferences
csharp_prefer_braces = true:silent
csharp_prefer_simple_using_statement = true:suggestion
csharp_style_namespace_declarations = block_scoped:silent
csharp_style_namespace_declarations = file_scoped:error
# Expression-level preferences
csharp_prefer_simple_default_expression = true:suggestion
@@ -287,4 +290,6 @@ csharp_style_prefer_top_level_statements = true:silent
csharp_style_prefer_utf8_string_literals = true:suggestion
csharp_style_prefer_readonly_struct = true:suggestion
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
csharp_style_prefer_primary_constructors = true:silent
csharp_prefer_system_threading_lock = true:suggestion
+191
View File
@@ -0,0 +1,191 @@
# Copilot Instructions for Wino-Mail Project
## Project Overview
Wino Mail is a native Windows mail client targeting Windows 10 1809+ and Windows 11. The project is **transitioning from UWP to WinUI 3** - always work with WinUI projects (Wino.Mail.WinUI, Wino.Core.WinUI), never edit the old Wino.Mail UWP project.
### Key Technologies
- **WinUI 3** for UI (previously UWP/WinUI 2)
- **MVVM Toolkit** (CommunityToolkit.Mvvm) for ViewModels with source generators
- **Messenger** pattern (WeakReferenceMessenger.Default) for event pub-sub throughout the codebase
- **SQLite** database stored in publisher cache folder (not local storage)
- **WebView2** for mail rendering/composition with custom HTML/JavaScript editors
- **MimeKit/MailKit** for IMAP/SMTP operations
- **Microsoft Graph SDK** for Outlook synchronization
- **Gmail API** for Gmail synchronization
### Solution Structure
```
Wino.Core.Domain → Entities, interfaces, translations, enums (shared contracts)
Wino.Core → Synchronization engine, authenticators, request processing
Wino.Services → Database, mail, folder, account services
Wino.Mail.ViewModels → Mail-specific ViewModels
Wino.Core.ViewModels → Shared ViewModels (settings, personalization)
Wino.Mail.WinUI → **Active WinUI 3 UI project** (use this)
Wino.Mail → **Deprecated UWP project** (DO NOT EDIT)
```
## Architecture Patterns
### Mail Synchronization Flow
1. **WinoRequestDelegator** → Validates and delegates user actions (mark read, delete, move)
2. **WinoRequestProcessor** → Batches requests using RequestComparer, queues to synchronizers
3. **Synchronizers** (OutlookSynchronizer, GmailSynchronizer, ImapSynchronizer) → Execute batched operations
4. **ChangeProcessors** (OutlookChangeProcessor, etc.) → Apply changes to local database
5. Database updates trigger **Messenger** events (MailAddedMessage, MailUpdatedMessage, etc.)
### Queue-Based Sync (New Pattern - See QUEUE_SYNC_IMPLEMENTATION.md)
- Initial sync now queues mail IDs first (MailItemQueue table), downloads metadata only (no MIME)
- MIME content downloaded on-demand when user opens mail
- Synchronizers override `QueueMailIdsForInitialSyncAsync()`, `DownloadMailsFromQueueAsync()`, `CreateMinimalMailCopyAsync()`
- Check `MailItemFolder.IsInitialSyncCompleted` to determine sync state
### Dependency Injection Setup
Services registered in extension methods across projects:
- `RegisterCoreServices()` in Wino.Core/CoreContainerSetup.cs
- `RegisterSharedServices()` in Wino.Services/ServicesContainerSetup.cs
- `RegisterCoreUWPServices()` in CoreUWPContainerSetup.cs
- ViewModels registered in App.xaml.cs with AddTransient/AddSingleton
### Messenger Pattern (Event Pub-Sub)
- All ViewModels inherit from CoreBaseViewModel or MailBaseViewModel which implement IRecipient<T>
- Register/unregister message handlers in `RegisterRecipients()` / `UnregisterRecipients()`
- Send messages via `WeakReferenceMessenger.Default.Send(new MessageType(...))`
- Common messages: MailAddedMessage, MailUpdatedMessage, NavigationRequested, ThemeChanged
## ViewModels Development Guidelines
### Observable Properties - Critical Pattern
- **ALWAYS** use `public partial` observable properties with MVVM Toolkit source generators
- **NEVER** use private fields with `[ObservableProperty]` attribute
- **Correct:**
```csharp
[ObservableProperty]
public partial string SearchQuery { get; set; } = string.Empty;
```
- **Incorrect:**
```csharp
[ObservableProperty]
private string searchQuery = string.Empty; // WRONG - will not work
```
### ViewModels Structure
- Inherit from MailBaseViewModel (for mail features) or CoreBaseViewModel (for shared features)
- Use `[RelayCommand]` for command methods - source generator creates Command properties
- Implement IRecipient<TMessage> for message handlers
- Use `IMailDialogService` for Mail-related dialogs, `IDialogServiceBase` for core dialogs
- Call `RegisterRecipients()` in constructor/OnNavigatedTo, `UnregisterRecipients()` in OnNavigatedFrom
## Localization System
### Translation Workflow (Custom T4-based System)
1. Add English strings ONLY to `Wino.Core.Domain/Translations/en_US/resources.json`
2. Build the project - source generators automatically create Translator properties
3. Use `Translator.{PropertyName}` in ViewModels, XAML (with x:Bind, OneTime mode)
4. **NEVER** edit other language files - Crowdin manages translations automatically
5. **NEVER** hardcode user-facing strings
### Usage Examples
```csharp
// ViewModel
_dialogService.InfoBarMessage(Translator.Info_MissingFolderTitle, message);
// XAML
<TextBlock Text="{x:Bind Translator.Settings_Title, Mode=OneTime}" />
```
## UI Data Binding and Converters
### WinUI 3 Automatic Conversions
- **NEVER** create IValueConverter classes or add them to Converters.xaml
- **NEVER** use BoolToVisibilityConverter - WinUI 3 SDK automatically converts bool to Visibility
- Direct binding: `Visibility="{x:Bind IsVisible, Mode=OneWay}"`
- Register control events (for example `Loaded`, `Unloaded`, `SizeChanged`, `PointerEntered`) in XAML markup, not with `+=` in `.xaml.cs`.
### XamlHelpers for Complex Conversions
- **ALWAYS** use XamlHelpers static methods instead of converters
- Add xmlns: `xmlns:helpers="using:Wino.Helpers"`
- Usage: `{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(PropertyName), Mode=OneWay}`
- Available methods: ReverseBoolToVisibilityConverter, CountToBooleanConverter, BoolToSelectionMode, Base64ToBitmapImage
- Add new methods to XamlHelpers.cs when needed, don't create converters
## WebView2 Mail Rendering
### Architecture
- **reader.html** (Wino.Mail.WinUI/JS/) for reading mails
- **editor.html** for composing mails (uses Jodit editor, not Quill as originally planned)
- WebView2 uses virtual host mapping: `https://wino.mail/reader.html`
- JavaScript interop via `ExecuteScriptFunctionAsync()` to call functions like `RenderHTML()`
- MIME content downloaded on-demand, not during sync
### Key Patterns
- Set environment variables for WebView2 before initialization (overlay scrollbars, cache)
- Wait for DOMContentLoaded event before script execution
- Handle theme changes by updating editor CSS dynamically
- Cancel external navigation, open in browser via Launcher.LaunchUriAsync()
## File Structure and Project Organization
### Critical Rules
- **NEVER** edit files in Wino.Mail (UWP) project - it's deprecated
- **ALWAYS** work with Wino.Mail.WinUI for UI components
- Place ViewModels in Wino.Mail.ViewModels (mail-specific) or Wino.Core.ViewModels (shared)
- Create abstract base classes in Views/Abstract folders
- Mail-specific dialog services go in Wino.Mail.WinUI/Services
### Database and Storage
- SQLite database in publisher cache folder (not app local storage)
- EML files stored in app local storage, referenced by MailCopy.FileId
- Paths resolved via MimeFileService.GetMimeMessagePath()
- Database entities in Wino.Core.Domain/Entities
## Error Handling and User Feedback
### Exception Handling Patterns
```csharp
try {
await operation();
} catch (UnavailableSpecialFolderException ex) {
_dialogService.InfoBarMessage(title, message, InfoBarMessageType.Warning, buttonText, action);
} catch (NotImplementedException) {
_dialogService.ShowNotSupportedMessage();
}
```
### Dialog Service Methods
- `InfoBarMessage()` - simple notifications with optional action button
- `ShowConfirmationDialogAsync()` - yes/no dialogs
- `PickFilesAsync()` - file selection
- Always check for null/empty results from dialog operations
## Code Style and Best Practices
- Use `var` where type is obvious from right side
- String interpolation over string.Format for simple cases
- Keep methods focused and single-responsibility
- Add XML documentation for public APIs
- Avoid introducing new NuGet packages - maximize use of existing libraries
- Wrap async operations in try-catch blocks
- Log errors via IWinoLogger but don't expose technical details to users
## Development Workflow
### Building and Running
- Open WinoMail.slnx in Visual Studio 2022+
- Target platforms: x86, x64, ARM64 (ARM32 being phased out)
- Minimum: Windows 10 1809, Target: Windows 11 22H2
- Set Wino.Mail.WinUI as startup project
### Testing
- Test suite in Wino.Core.Tests
- Manual testing required for UI/WebView2 interactions
- Test synchronization with real accounts when modifying synchronizers
### Common Pitfalls
- Forgetting to register ViewModels in App.xaml.cs RegisterViewModels()
- Not calling RegisterRecipients() for message handlers
- Using private fields with [ObservableProperty] (won't work - must be public partial)
- Creating IValueConverter classes instead of using XamlHelpers
- Editing UWP project files instead of WinUI equivalents
- Hardcoding strings instead of using Translator
- Forgetting to unregister Messenger recipients (memory leaks)
+187
View File
@@ -0,0 +1,187 @@
name: Manual Beta Release
on:
workflow_dispatch:
inputs:
release_title:
description: Optional GitHub release title override
required: false
type: string
permissions:
contents: write
packages: read
jobs:
release-beta:
name: Build and publish beta release
runs-on: windows-latest
env:
PROJECT_PATH: Wino.Mail.WinUI/Wino.Mail.WinUI.csproj
MANIFEST_PATH: Wino.Mail.WinUI/Package.appxmanifest
CHANGELOG_PATH: CHANGELOG.md
NUGET_CONFIG_PATH: ${{ github.workspace }}\nuget.config
PACKAGE_OUTPUT_DIR: ${{ github.workspace }}\artifacts\package
RELEASE_OUTPUT_DIR: ${{ github.workspace }}\artifacts\release
CERTIFICATE_PFX_PATH: ${{ github.workspace }}\artifacts\signing\beta-signing-cert.pfx
CERTIFICATE_CER_PATH: ${{ github.workspace }}\artifacts\release\Wino-Mail-Beta.cer
steps:
- name: Checkout selected branch
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
fetch-depth: 0
- name: Fetch tags from origin
shell: pwsh
run: git fetch origin --force --tags
- name: Validate release secrets
shell: pwsh
env:
BETA_SIGNING_CERT_PFX_BASE64: ${{ secrets.BETA_SIGNING_CERT_PFX_BASE64 }}
run: |
if ([string]::IsNullOrWhiteSpace($env:BETA_SIGNING_CERT_PFX_BASE64)) {
throw "Missing required secret: BETA_SIGNING_CERT_PFX_BASE64"
}
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
env:
NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Compute beta version and release metadata
id: metadata
shell: pwsh
env:
RELEASE_TITLE_INPUT: ${{ github.event.inputs.release_title }}
run: |
$manifestPath = Join-Path $env:GITHUB_WORKSPACE $env:MANIFEST_PATH
if (-not (Test-Path $manifestPath)) {
throw "Package manifest not found: $manifestPath"
}
$changelogPath = Join-Path $env:GITHUB_WORKSPACE $env:CHANGELOG_PATH
if (-not (Test-Path $changelogPath)) {
throw "Release notes file not found: $changelogPath"
}
[xml]$manifest = Get-Content -LiteralPath $manifestPath
$identityNode = $manifest.Package.Identity
if (-not $identityNode) {
throw "Could not locate the Package/Identity node in $manifestPath"
}
$currentVersionText = [string]$identityNode.Version
if ($currentVersionText -notmatch '^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)\.(?<revision>\d+)$') {
throw "Manifest version '$currentVersionText' is not a four-part numeric version."
}
$packageVersion = $currentVersionText
$releaseTag = "v$packageVersion"
$releaseTitleInput = $env:RELEASE_TITLE_INPUT
$releaseTitle = if ([string]::IsNullOrWhiteSpace($releaseTitleInput)) { $releaseTag } else { $releaseTitleInput.Trim() }
$headSha = (git rev-parse HEAD).Trim()
if ([string]::IsNullOrWhiteSpace($headSha)) {
throw "Failed to resolve the checked out commit SHA."
}
$notesInput = Get-Content -LiteralPath $changelogPath -Raw
if ([string]::IsNullOrWhiteSpace($notesInput)) {
throw "Release notes file is empty: $changelogPath"
}
$notesInput = $notesInput.Trim()
New-Item -ItemType Directory -Path $env:RELEASE_OUTPUT_DIR -Force | Out-Null
$releaseNotesPath = Join-Path $env:RELEASE_OUTPUT_DIR 'beta-release-notes.md'
$notesInput | Set-Content -LiteralPath $releaseNotesPath -Encoding utf8
"package_version=$packageVersion" >> $env:GITHUB_OUTPUT
"release_tag=$releaseTag" >> $env:GITHUB_OUTPUT
"release_title=$releaseTitle" >> $env:GITHUB_OUTPUT
"release_notes_path=$releaseNotesPath" >> $env:GITHUB_OUTPUT
"head_sha=$headSha" >> $env:GITHUB_OUTPUT
- name: Materialize signing certificate
shell: pwsh
env:
BETA_SIGNING_CERT_PFX_BASE64: ${{ secrets.BETA_SIGNING_CERT_PFX_BASE64 }}
run: |
$signingDir = Split-Path -Parent $env:CERTIFICATE_PFX_PATH
New-Item -ItemType Directory -Path $signingDir -Force | Out-Null
[IO.File]::WriteAllBytes($env:CERTIFICATE_PFX_PATH, [Convert]::FromBase64String($env:BETA_SIGNING_CERT_PFX_BASE64))
$certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($env:CERTIFICATE_PFX_PATH, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)
New-Item -ItemType Directory -Path (Split-Path -Parent $env:CERTIFICATE_CER_PATH) -Force | Out-Null
[IO.File]::WriteAllBytes($env:CERTIFICATE_CER_PATH, $certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert))
- name: Restore WinUI project dependencies
shell: pwsh
run: |
if (-not (Test-Path $env:NUGET_CONFIG_PATH)) {
throw "NuGet config file not found: $env:NUGET_CONFIG_PATH"
}
dotnet restore $env:PROJECT_PATH `
--configfile $env:NUGET_CONFIG_PATH `
-p:Platform=x64 `
/p:RestoreConfigFile="$env:NUGET_CONFIG_PATH"
- name: Build MSIX bundle
shell: pwsh
run: |
New-Item -ItemType Directory -Path $env:PACKAGE_OUTPUT_DIR -Force | Out-Null
dotnet build $env:PROJECT_PATH `
--configuration Release `
--no-restore `
--configfile $env:NUGET_CONFIG_PATH `
/p:Platform=x64 `
/p:RestoreConfigFile="$env:NUGET_CONFIG_PATH" `
/p:GenerateAppxPackageOnBuild=true `
/p:UapAppxPackageBuildMode=SideloadOnly `
/p:AppxBundle=Always `
/p:AppxBundlePlatforms="x86|x64|arm64" `
/p:AppxPackageDir="$env:PACKAGE_OUTPUT_DIR\\" `
/p:AppxPackageVersion=${{ steps.metadata.outputs.package_version }} `
/p:PackageCertificateKeyFile="$env:CERTIFICATE_PFX_PATH" `
/p:PackageCertificatePassword= `
/p:PackageCertificateThumbprint= `
/p:AppxPackageSigningEnabled=true
- name: Collect packaged artifacts
id: package
shell: pwsh
run: |
$bundle = Get-ChildItem -Path $env:PACKAGE_OUTPUT_DIR -Recurse -Filter *.msixbundle | Select-Object -First 1
if (-not $bundle) {
throw "No .msixbundle file was generated under $env:PACKAGE_OUTPUT_DIR"
}
$releaseAssetPath = Join-Path $env:RELEASE_OUTPUT_DIR "Wino_${{ steps.metadata.outputs.package_version }}.zip"
if (Test-Path $releaseAssetPath) {
Remove-Item -LiteralPath $releaseAssetPath -Force
}
Compress-Archive -LiteralPath @($bundle.FullName, $env:CERTIFICATE_CER_PATH) -DestinationPath $releaseAssetPath -Force
"bundle_path=$($bundle.FullName)" >> $env:GITHUB_OUTPUT
"bundle_name=$($bundle.Name)" >> $env:GITHUB_OUTPUT
"release_asset_path=$releaseAssetPath" >> $env:GITHUB_OUTPUT
- name: Create GitHub prerelease
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "${{ steps.metadata.outputs.release_tag }}" `
"${{ steps.package.outputs.release_asset_path }}" `
--repo "${{ github.repository }}" `
--target "${{ steps.metadata.outputs.head_sha }}" `
--title "${{ steps.metadata.outputs.release_title }}" `
--notes-file "${{ steps.metadata.outputs.release_notes_path }}" `
--prerelease
+2 -3
View File
@@ -206,9 +206,6 @@ PublishScripts/
*.nuget.props
*.nuget.targets
# Nuget personal access tokens and Credentials
nuget.config
# Microsoft Azure Build Output
csx/
*.build.csdef
@@ -402,3 +399,5 @@ Wino/obj/x86/Debug/XamlSaveStateFile.xml
Wino/obj/x86/Debug/XamlSaveStateFile.xml
*.cache
.vs/Wino/v16/.suo
/.claude/settings.local.json
scripts/translate_resources.local.bat
+163
View File
@@ -0,0 +1,163 @@
# AGENTS.md
This file provides guidance to AI agent when working with code in this repository.
## Project Overview
Wino Mail is a native Windows mail client (Windows 10 1809+ / Windows 11) replacing the deprecated Windows Mail & Calendar. It's **transitioning from UWP to WinUI 3** - always work with WinUI projects (Wino.Mail.WinUI), never edit the deprecated Wino.Mail UWP project.
## Build and Development Commands
```bash
# Open solution
# WinoMail.slnx is the main solution file (VS 2022+)
# Build WinUI project (Debug x64)
dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config -p:Platform=x64 -p:RuntimeIdentifier=win-x64 && dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false
# Build WinUI project with diagnostic XAML/compiler logging (use when plain build only shows "XamlCompiler.exe exited with code 1")
dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false "/flp:logfile=winui-build.log;verbosity=diagnostic" /bl:winui-build.binlog
# Run tests (Debug x64)
dotnet test Wino.Core.Tests/Wino.Core.Tests.csproj -c Debug /p:Platform=x64
# Copilot CLI build command (Debug x64)
dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config -p:Platform=x64 -p:RuntimeIdentifier=win-x64 && dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false
```
**Prerequisites:** Visual Studio 2022+ with ".NET desktop development" workload, .NET SDK 10+
**Startup project:** Wino.Mail.WinUI
**Platforms:** x86, x64, ARM64
## Efficient Workflow
- Start with targeted symbol or file search before reading full files
- Prefer one focused task per thread; use a new thread for unrelated follow-up work
- Keep verification narrow: build only the affected project, not the full solution, unless cross-project changes require it
- After the first restore, prefer `--no-restore` builds unless package or project references changed
- Summarize long build logs and inspect only the files named in diagnostics instead of loading large logs into context
- When the prompt already names likely files, types, or symbols, start there instead of re-mapping the repository
- If a WinUI build only reports `XamlCompiler.exe exited with code 1`, rerun with the diagnostic logging command above and inspect the terminal output plus `winui-build.log` for real `WMC`/`WMC1121`/binding diagnostics before guessing
## Architecture
### Solution Structure
```
Wino.Core.Domain → Entities, interfaces, translations, enums (shared contracts)
Wino.Core → Synchronization engine, authenticators, request processing
Wino.Services → Database, mail, folder, account services
Wino.Authentication → OAuth2 authenticators (Outlook, Gmail)
Wino.Mail.ViewModels → Mail-specific ViewModels
Wino.Core.ViewModels → Shared ViewModels (settings, personalization)
Wino.Messaging → Pub-sub message definitions
Wino.Mail.WinUI → **Active WinUI 3 UI project** (use this)
Wino.Mail → **Deprecated UWP project** (DO NOT EDIT)
```
### Mail Synchronization Flow
1. **WinoRequestDelegator** → Validates and delegates user actions (mark read, delete, move)
2. **WinoRequestProcessor** → Batches requests using RequestComparer, queues to synchronizers
3. **Synchronizers** (OutlookSynchronizer, GmailSynchronizer, ImapSynchronizer) → Execute batched operations
4. **ChangeProcessors** → Apply changes to local SQLite database
5. Database updates trigger **Messenger** events (MailAddedMessage, MailUpdatedMessage, etc.)
### Synchronizer Types
- **OutlookSynchronizer** - Microsoft Graph SDK for Office 365
- **GmailSynchronizer** - Gmail API
- **ImapSynchronizer** - MimeKit/MailKit for IMAP/SMTP
### Queue-Based Sync Pattern
- Initial sync queues mail IDs first (MailItemQueue table), downloads metadata only
- MIME content downloaded on-demand when user opens mail
- Check `MailItemFolder.IsInitialSyncCompleted` for sync state
- See QUEUE_SYNC_IMPLEMENTATION.md for details
### Dependency Injection
- `RegisterCoreServices()` in Wino.Core/CoreContainerSetup.cs
- `RegisterSharedServices()` in Wino.Services/ServicesContainerSetup.cs
- ViewModels registered in App.xaml.cs
## Key Patterns
### MVVM with Source Generators
**CORRECT - use public partial properties:**
```csharp
[ObservableProperty]
public partial string SearchQuery { get; set; } = string.Empty;
```
**WRONG - will not work:**
```csharp
[ObservableProperty]
private string searchQuery = string.Empty;
```
### Messenger Pattern
- ViewModels inherit from CoreBaseViewModel or MailBaseViewModel
- Register handlers in `RegisterRecipients()`, unregister in `UnregisterRecipients()`
- Send via `WeakReferenceMessenger.Default.Send(new MessageType(...))`
### Data Binding - No Converters
- **NEVER** create IValueConverter classes
- WinUI 3 auto-converts bool to Visibility: `Visibility="{x:Bind IsVisible, Mode=OneWay}"`
- Use XamlHelpers for complex conversions: `{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(Prop)}`
- `x:Bind` does not implicitly convert `double` to `GridLength`; when binding `RowDefinition.Height` or `ColumnDefinition.Width`, use a `XamlHelpers` method such as `DoubleToGridLength(...)`
- For `ComboBox` controls in XAML, never use `DisplayMemberPath` or `SelectedValuePath`; use a typed `ItemTemplate` and bind `SelectedItem` explicitly, preferably with `x:Bind`
## Localization
1. Add English strings ONLY to Wino.Core.Domain/Translations/en_US/resources.json
2. Build project - source generators create Translator properties
3. Use Translator.{PropertyName} in code/XAML
4. NEVER edit any resources.json file outside Wino.Core.Domain/Translations/en_US/resources.json
5. Treat all non-en_US translation files as managed externally and leave them untouched, even when adding new localization keys
6. In XAML, translation bindings must use `Mode=OneTime` because `Wino.Core.Domain/Translator.cs` does not implement `INotifyPropertyChanged`
## Storage
- **SQLite database** in publisher cache folder (shared with future Wino Calendar)
- **EML files** in app local storage, referenced by `MailCopy.FileId`
- Paths resolved via `MimeFileService.GetMimeMessagePath()`
## WebView2 Mail Rendering
- `reader.html` for reading mails, `editor.html` for composing (Jodit editor)
- Virtual host mapping: `https://wino.mail/reader.html`
- JavaScript interop via `ExecuteScriptFunctionAsync()`
- MIME content downloaded on-demand, not during sync
## Common Pitfalls
- Forgetting to register ViewModels in App.xaml.cs `RegisterViewModels()`
- Not calling `RegisterRecipients()` for message handlers
- Using private fields with `[ObservableProperty]` instead of public partial
- Creating IValueConverter classes instead of using XamlHelpers
- Editing UWP project files instead of WinUI equivalents
- Hardcoding strings instead of using Translator
- Forgetting to unregister Messenger recipients (memory leaks)
- Putting authentication validation, token refresh, account API calls, settings serialization/deserialization, or preference-application logic into ViewModels instead of the corresponding service
## Code Style
- Avoid introducing new NuGet packages when possible
- Use existing libraries (MimeKit, MailKit, Microsoft Graph, Gmail API)
- Use `var` where type is obvious
- String interpolation over string.Format
- Wrap async operations in try-catch
- Log errors via IWinoLogger
- For dependency properties in WinUI code, always prefer `[GeneratedDependencyProperty]` from CommunityToolkit over manual `DependencyProperty.Register(...)` declarations.
- When a `[RelayCommand]` needs enable/disable logic, prefer the command's `CanExecute` over binding `Button.IsEnabled` in XAML; use `[NotifyCanExecuteChangedFor]` on dependent properties and call `NotifyCanExecuteChanged()` explicitly when non-generated state affects the command.
- In ViewModels, update all UI-bound properties/collections via `ExecuteUIThread(...)` (especially after awaited calls and any use of `ConfigureAwait(false)`).
- `ConfigureAwait(false)` continues execution on a background thread. Any UI-bound property change, `INotifyPropertyChanged` notification, collection mutation, or similar UI-facing state update after that point must be marshaled back with `ExecuteUIThread(...)` or the appropriate dispatcher call, otherwise the app can crash.
- Messenger messages are raised from a background thread by default, while UI control event handlers such as `Button.Click` start on the UI thread. Be deliberate when combining dispatcher usage with `ConfigureAwait(false)` so post-await UI updates always return to the UI thread.
- ViewModels should only handle UI interaction/state and delegate business logic to services; account-management work belongs in `WinoAccountProfileService`, and preferences import/export/apply logic belongs in `PreferencesService`.
- In `EventDetailsPageViewModel.LoadAttendeesAsync`, never mutate `CurrentEvent.Attendees` outside `ExecuteUIThread(...)`.
- Never create pure C# controls or controls that heavily manipulate UI structure from `.cs` files. Define controls in XAML and keep UI composition in XAML.
- Never add XAML-backed UI controls to `.xaml.cs`. If a view has XAML, all control declarations, flyouts, templates, and visual composition belong in the `.xaml` file; keep `.xaml.cs` limited to event handling and view glue.
- Never subscribe to framework events like `Loaded`, `Unloaded`, or input events from constructors in `.xaml.cs` for XAML-backed controls and pages; wire them directly in XAML instead.
- If you use `x:Load` in XAML, always give that `UIElement` an `x:Name`.
+6 -1
View File
@@ -15,7 +15,12 @@ Wino Mail is [Universal Windows Platform](https://learn.microsoft.com/en-us/wind
**Min Version:** Windows 10 1809
**Target Version:** Windows 11 22H2
It's pretty straightforward after cloning the repo. There are no prerequisites needed. Just open **Wino.sln** solution in your IDE and launch.
## Prerequisites
* ".NET desktop development" workload in Visual Studio 2022+
* .NET SDK 8.0+
With those installed, it's pretty straightforward after cloning the repo. Just open **Wino.sln** solution in your IDE and launch.
## Project Architecture
+7
View File
@@ -0,0 +1,7 @@
<Project>
<PropertyGroup>
<LangVersion>preview</LangVersion>
<IsAotCompatible>true</IsAotCompatible>
<Configurations>Debug;Release</Configurations>
</PropertyGroup>
</Project>
+80
View File
@@ -0,0 +1,80 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="ColorHashSharp" Version="1.1.0" />
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.2" />
<PackageVersion Include="CommunityToolkit.Diagnostics" Version="8.4.2" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.TokenizingTextBox" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.250926-build.2293" />
<PackageVersion Include="CommunityToolkit.WinUI.Lottie" Version="8.2.250604" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.DependencyPropertyGenerator" Version="0.1.250926-build.2293" />
<PackageVersion Include="EmailValidation" Version="1.3.0" />
<PackageVersion Include="gravatar-dotnet" Version="0.1.3" />
<PackageVersion Include="HtmlAgilityPack" Version="1.12.4" />
<PackageVersion Include="Ical.Net" Version="5.2.1" />
<PackageVersion Include="IsExternalInit" Version="1.0.3" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Graph" Version="5.104.0" />
<PackageVersion Include="Microsoft.Graphics.Win2D" Version="1.4.0" />
<PackageVersion Include="Microsoft.Identity.Client" Version="4.83.3" />
<PackageVersion Include="Microsoft.Identity.Client.Broker" Version="4.83.3" />
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.83.3" />
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.14" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools.MSIX" Version="1.7.260316102" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.1" />
<PackageVersion Include="Wino.Mail.Contracts" Version="1.0.18" />
<PackageVersion Include="MimeKit" Version="4.16.0" />
<PackageVersion Include="morelinq" Version="4.4.0" />
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
<PackageVersion Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
<PackageVersion Include="NodaTime" Version="3.3.1" />
<PackageVersion Include="Sentry.Serilog" Version="6.4.0" />
<PackageVersion Include="Serilog" Version="4.3.1" />
<PackageVersion Include="Serilog.Exceptions" Version="8.4.0" />
<PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="Serilog.Sinks.ApplicationInsights" Version="4.0.0" />
<PackageVersion Include="SkiaSharp" Version="3.119.2" />
<PackageVersion Include="SkiaSharp.Views.WinUI" Version="3.119.2" />
<PackageVersion Include="sqlite-net-pcl" Version="1.10.196-beta" />
<PackageVersion Include="System.Drawing.Common" Version="10.0.5" />
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" />
<PackageVersion Include="System.Text.Json" Version="10.0.6" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.0" />
<PackageVersion Include="H.NotifyIcon.WinUI" Version="2.4.1" />
<PackageVersion Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
<PackageVersion Include="Google.Apis.Auth" Version="1.73.0" />
<PackageVersion Include="Google.Apis.Calendar.v3" Version="1.73.0.4073" />
<PackageVersion Include="Google.Apis.Drive.v3" Version="1.73.0.4112" />
<PackageVersion Include="Google.Apis.Gmail.v1" Version="1.73.0.4029" />
<PackageVersion Include="Google.Apis.PeopleService.v1" Version="1.72.0.3973" />
<PackageVersion Include="HtmlKit" Version="1.2.0" />
<PackageVersion Include="MailKit" Version="4.16.0" />
<PackageVersion Include="TimePeriodLibrary.NET" Version="2.1.6" />
<PackageVersion Include="System.Reactive" Version="6.1.0" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.6" />
<PackageVersion Include="System.Text.Encodings.Web" Version="10.0.6" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1721" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.0.250930001-experimental1" />
<PackageVersion Include="WinUIEx" Version="2.9.0" />
<!-- Testing packages -->
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="FluentAssertions" Version="8.9.0" />
<PackageVersion Include="Moq" Version="4.20.72" />
</ItemGroup>
</Project>
+30 -21
View File
@@ -1,45 +1,54 @@
<p align="center">
<a href="https://apps.microsoft.com/detail/Wino%20Mail/9NCRCVJC50WL?launch=true
&mode=full">
<img src="https://www.winomail.app/images/wino_logo.png" width=90 height=90>
<a href="https://apps.microsoft.com/detail/Wino%20Mail/9NCRCVJC50WL?launch=true&mode=full">
<img src="https://www.winomail.app/images/v2/Logo.png" width="90" height="90" alt="Wino Mail logo">
</a>
<h3 align="center">Wino Mail</h3>
<p align="center">
Native mail client for Windows device families.
Native mail and calendar client for Windows.
</p>
</p>
<br>
![pdark](https://user-images.githubusercontent.com/12009960/232114528-2d2c8e3c-dbe7-429a-94e0-6aecc73bdf70.png)
![Wino Mail screenshot](https://user-images.githubusercontent.com/12009960/232114528-2d2c8e3c-dbe7-429a-94e0-6aecc73bdf70.png)
## Motivation
I'm a big fan of Windows Mail & Calendars due to its simplicity. Personally, I find it more intuitive for daily use cases compared to Outlook desktop and the new WebView2 powered Outlook version. Seeing [Microsoft deprecating it](https://support.microsoft.com/en-us/office/outlook-for-windows-the-future-of-mail-calendar-and-people-on-windows-11-715fc27c-e0f4-4652-9174-47faa751b199#:~:text=The%20Mail%20and%20Calendar%20applications,will%20no%20longer%20be%20supported.) dragged me into starting to work on Wino a couple of years ago. Wino's main motivation is to bring all the existing functionality from Mail & Calendars over time without changing the user experience that millions have loved since the Windows 8 days in Mail & Calendars
I'm a big fan of Windows Mail & Calendars due to its simplicity. Personally, I find it more intuitive for daily use cases compared to Outlook desktop and the new WebView2 powered Outlook version. Seeing [Microsoft deprecating it](https://support.microsoft.com/en-us/office/outlook-for-windows-the-future-of-mail-calendar-and-people-on-windows-11-715fc27c-e0f4-4652-9174-47faa751b199#:~:text=The%20Mail%20and%20Calendar%20applications,will%20no%20longer%20be%20supported.) dragged me into starting to work on Wino a couple of years ago. Wino's main motivation is to bring all the existing functionality from Mail & Calendars over time without changing the user experience that millions have loved since the Windows 8 days in Mail & Calendars.
## vNext Release Highlights
Wino vNext focuses on making Mail, Calendar, and Contacts feel like one cohesive native Windows experience while improving sync reliability and startup responsiveness.
- 📅 **Calendar management:** Event compose/create flow, calendar-mail mapping, reminder snooze support, occurrence and detail-page improvements, and CalDAV correctness fixes.
- 👥 **Contact management:** Improved contact workflows, account/settings integration, and contact data-model cleanup.
- 🔄 **Synchronization reliability:** Refactored synchronizers, better state handling, 404 + 429 error handling, and duplicate-operation prevention.
- ✉️ **Compose and drafts:** Refined editor/toolbar architecture, better rendering pipeline, Gmail draft support, and large Outlook attachment upload sessions.
-**Performance and quality:** Faster mail fetching with batched DB queries and caching, SQLite indexing/foreign key enforcement, and broader test + CI coverage.
- 🎨 **WinUI polish:** Improved onboarding/startup, settings and dialogs refresh, notification routing fixes, and keyboard/navigation quality-of-life improvements.
## Features
- API integration for Outlook and Gmail
- IMAP/SMTP support for custom mail servers
- Send, receive, mark as (read,important,spam etc), move mails.
- Linked/Merged Accounts
- Toast notifications with background sync.
- Instant startup performance
- Offline use / search.
- Modern and responsive UI
- Lots of personalization options
- Dark / Light mode for mail reader
- 📨 Outlook and Gmail API integration
- 🌐 IMAP/SMTP support for custom mail servers
- 📅 Calendar support with event creation/compose and reminders
- 👥 Contact management and people-centric account experience
- ✅ Core mail actions: send, receive, read/unread, move, spam, and more
- 🔗 Linked/Merged accounts
- 🔔 Toast notifications with background sync
- ⚡ Instant startup-oriented architecture
- 🔎 Offline-capable workflows and search improvements
- 🎛️ Modern responsive WinUI interface with personalization options
- 🌗 Dark/Light mode for mail reader and app surfaces
## Download
Download latest version of Wino Mail from Microsoft Store for free.
<a href="https://apps.microsoft.com/detail/Wino%20Mail/9NCRCVJC50WL?launch=true
&mode=full">
<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200"/>
<a href="https://apps.microsoft.com/detail/Wino%20Mail/9NCRCVJC50WL?launch=true&mode=full">
<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200" alt="Get Wino Mail from Microsoft Store"/>
</a>
## Beta Releases
@@ -48,7 +57,6 @@ Stable releases will always be distributed on Microsoft Store. However, beta rel
These releases are distributed as side-loaded packages. To install them, download the **.msixbundle** file in GitHub releases and [follow the steps explained here.](https://learn.microsoft.com/en-us/windows/application-management/sideload-apps-in-windows)
## Contributing
Check out the [contribution guidelines](/CONTRIBUTING.md) before diving into the source code or opening an issue. There are multiple ways to contribute and all of them are explained in detail there.
@@ -59,3 +67,4 @@ Your donations will motivate me more to work on Wino in my spare time and cover
- You can [donate via Paypal by clicking here](https://www.paypal.com/donate/?hosted_button_id=LGPERGGXFMQ7U)
- You can buy Unlimited Accounts add-on in the application. It's a one-time payment for lifetime, not a monthly recurring payment.
+158
View File
@@ -0,0 +1,158 @@
# Wino Mail vNext Improvements
This document summarizes the major improvements on `feature/vNext` compared to `main`, based on the commit history between the current branch and the merge-base with `main`.
## Wino Calendar
Calendar has grown from an early implementation into a much more complete product area on this branch.
### A full Wino Calendar experience
- Added a dedicated Wino Calendar app entry, making calendar a first-class experience instead of a secondary add-on.
- Built out the calendar rendering experience with multiple rounds of rendering improvements, updated calendar view styling, calendar buttons, and better event visuals.
- Added event creation and full event compose flows, including follow-up improvements for attachments, attendees, recurrence summaries, RSVP actions, reminders, and event details.
- Improved support for all-day events, better display dates, occurrence handling, and mail-to-calendar mapping so calendar actions connect more naturally with messages and invitations.
### Local calendar support
- Added local calendar operation coverage and supporting behavior for IMAP-backed/local calendar scenarios.
- Prevented duplicate operations by ignoring local calendar apply-changes in the wrong paths.
- Added busy-state support and metadata fetch flows so newly created accounts can initialize calendar data more reliably.
### CalDAV sync
- Introduced a dedicated CalDAV synchronizer and supporting service/client work.
- Fixed CalDAV delta sync issues.
- Fixed CalDAV timezone issues.
- Added manual live CalDAV workflow tests to validate real-world sync behavior.
This means local and self-hosted calendar scenarios are much better represented on this branch than on `main`.
### API calendar sync for Outlook and Gmail
- Expanded Outlook calendar sync behavior, including broader sync windows and fixes around date/time handling.
- Improved Gmail drafting and mail/calendar integration so event-related actions work better across providers.
- Added mail and calendar synchronizer state tracking to make sync progress and error handling more reliable.
- Added auto calendar sync on account creation and broader auto-sync trigger and cancellation support.
### Calendar polish and reliability
- Fixed calendar crashes and null-handling issues in calendar view date range updates.
- Fixed double initialization in calendar day views.
- Improved reaction to calendar changes and calendar item update-source handling.
- Added reminder snooze support across toast UI, services, and database storage.
Overall, Wino Calendar is one of the biggest themes of this branch: richer UI, more complete event workflows, and real sync support across local, CalDAV, Outlook, and Gmail-backed scenarios.
## Wino Accounts
Wino Accounts was significantly expanded and polished on this branch.
### Account flows and identity
- Added sign in, sign out, and registration flows.
- Redesigned login and registration dialogs.
- Added privacy policy presentation during registration.
- Added forgot password and email confirmation flows.
- Pointed the app to the real API and improved profile caching.
### Account management and settings
- Added Wino account settings and a dedicated management page.
- Added a special navigation item for Wino Accounts.
- Added import functionality for Wino Accounts.
- Added a preference to hide the title bar Wino account button.
- Improved the top-shell account icon and signed-out identity visuals.
### Purchases and add-ons
- Added handling for Paddle purchases and add-ons.
- Added purchase-success deep linking.
- Added support for AI pack handling through the Microsoft Store.
### User-facing polish
- Redesigned the Wino Account flyout and menu with a more polished Fluent-style presentation.
- Improved account cleanup behavior when an account is deleted.
- Added account attention handling and better account details/settings behavior.
Compared to `main`, this branch turns Wino Accounts into a much more complete platform feature rather than a minimal sign-in surface.
## Improved Stability and Reliability
A large part of this branch is about making the app more dependable in everyday use.
### Synchronization stability
- Refactored synchronizers to address long-standing reliability issues.
- Improved thread mapping across synchronizers.
- Added generic 404 handling for synchronizers.
- Added specific Outlook 429 handling for rate-limit scenarios.
- Improved Outlook authentication and Outlook sync reliability.
- Improved Gmail synchronizer behavior.
- Added explicit mail and calendar synchronizer state support.
### Mail and data reliability
- Optimized mail fetching with batched database queries and in-memory caching.
- Added SQLite indexes and enabled foreign key enforcement.
- Switched away from the old mail item queue approach and returned to a simpler initial sync strategy.
- Improved local draft resend behavior and added grace-period handling for local drafts.
- Added better handling for large Outlook attachments via upload sessions.
- Fixed issues with sent/draft placement, loading mails with infinite scroll, selection cleanup, and deleted-object scenarios.
### UI and lifecycle stability
- Fixed mail rendering page disposal issues.
- Fixed WebView2 runtime toast dispatching on the UI thread.
- Fixed startup mode issues, single-instancing problems, and shell/navigation regressions.
- Fixed multiple thread selection, container, flicker, and context-menu issues.
- Fixed crashes and null-reference style issues in several calendar and shell flows.
### Engineering quality
- Added more tests across calendar, CalDAV, IMAP, view-model, sanitization, and account sync scenarios.
- Added a GitHub Actions workflow to build WinUI and run Core tests on pull requests.
- Resolved warnings and moved the WinUI project toward warnings-as-errors discipline.
- Added AOT compatibility work and related cleanup across the app.
The branch is not just adding features; it is also clearly reducing failure points throughout sync, rendering, navigation, and storage.
## Contacts, Settings, and General UX
This branch also improves the everyday product experience outside mail and calendar core flows.
### Contacts
- Added contacts management.
- Improved contacts UI and related thread/image preview behavior.
- Removed legacy SQLite base64 contact storage from `AccountContact`.
- Added contact picture handling support and supporting contact service improvements.
### Settings
- Added a dedicated settings shell and refactored settings home/navigation.
- Expanded settings UI and introduced new setting options.
- Added calendar settings into the settings experience.
- Improved account details/settings pages and storage settings navigation.
- Refined settings visuals, shell integration, and menu behavior.
### Onboarding and app experience
- Added a new startup window and a more guided onboarding flow with wizard-like steps.
- Added a "What's New" implementation for feature communication.
- Improved dialogs, title bar behavior, shell content, navigation, and shell polish across multiple iterations.
- Added live store update notifications.
- Improved keyboard shortcuts and related dialogs.
- Added tray icon support and better toast routing between mail and calendar app entries.
## Summary
Compared to `main`, `feature/vNext` delivers four major leaps:
1. Wino Calendar becomes a substantially more complete feature set, including local calendar support, CalDAV sync, and stronger Outlook and Gmail calendar integration.
2. Wino Accounts becomes a real product surface with better authentication flows, management, imports, purchases, and polish.
3. The app is more stable thanks to synchronization refactors, storage improvements, test expansion, and many crash and lifecycle fixes.
4. Contacts, settings, onboarding, and shell/navigation experience all feel more mature and more consistent.
In short, this branch is a broad product maturation release rather than a narrow feature drop.
+42
View File
@@ -0,0 +1,42 @@
{
"AttributesTolerance": 2,
"KeepFirstAttributeOnSameLine": false,
"MaxAttributeCharactersPerLine": 0,
"MaxAttributesPerLine": 1,
"NewlineExemptionElements": "RadialGradientBrush, GradientStop, LinearGradientBrush, ScaleTransform, SkewTransform, RotateTransform, TranslateTransform, Trigger, Condition, Setter",
"SeparateByGroups": false,
"AttributeIndentation": 0,
"AttributeIndentationStyle": 1,
"RemoveDesignTimeReferences": false,
"IgnoreDesignTimeReferencePrefix": false,
"EnableAttributeReordering": true,
"AttributeOrderingRuleGroups": [
"x:Class",
"xmlns, xmlns:x",
"xmlns:*",
"x:Key, Key, x:Name, Name, x:Uid, Uid, Title",
"Grid.Row, Grid.RowSpan, Grid.Column, Grid.ColumnSpan, Canvas.Left, Canvas.Top, Canvas.Right, Canvas.Bottom",
"Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight",
"Margin, Padding, HorizontalAlignment, VerticalAlignment, HorizontalContentAlignment, VerticalContentAlignment, Panel.ZIndex",
"*:*, *",
"PageSource, PageIndex, Offset, Color, TargetName, Property, Value, StartPoint, EndPoint",
"mc:Ignorable, d:IsDataSource, d:LayoutOverrides, d:IsStaticText",
"Storyboard.*, From, To, Duration"
],
"FirstLineAttributes": "",
"OrderAttributesByName": true,
"PutEndingBracketOnNewLine": false,
"RemoveEndingTagOfEmptyElement": true,
"SpaceBeforeClosingSlash": true,
"RootElementLineBreakRule": 0,
"ReorderVSM": 2,
"ReorderGridChildren": false,
"ReorderCanvasChildren": false,
"ReorderSetters": 0,
"FormatMarkupExtension": true,
"NoNewLineMarkupExtensions": "x:Bind, Binding",
"ThicknessSeparator": 2,
"ThicknessAttributes": "Margin, Padding, BorderThickness, ThumbnailClipMargin",
"FormatOnSave": true,
"CommentPadding": 2,
}
+16
View File
@@ -0,0 +1,16 @@
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Authentication;
public abstract class BaseAuthenticator
{
public abstract MailProviderType ProviderType { get; }
protected IAuthenticatorConfig AuthenticatorConfig { get; }
protected BaseAuthenticator(IAuthenticatorConfig authenticatorConfig)
{
AuthenticatorConfig = authenticatorConfig;
}
}
+50
View File
@@ -0,0 +1,50 @@
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Util.Store;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Authentication;
namespace Wino.Authentication;
public class GmailAuthenticator : BaseAuthenticator, IGmailAuthenticator
{
public GmailAuthenticator(IAuthenticatorConfig authConfig) : base(authConfig)
{
}
public string ClientId => AuthenticatorConfig.GmailAuthenticatorClientId;
public bool ProposeCopyAuthURL { get; set; }
public override MailProviderType ProviderType => MailProviderType.Gmail;
/// <summary>
/// Generates the token information for the given account.
/// For gmail, interactivity is automatically handled when you get the token.
/// </summary>
/// <param name="account">Account to get token for.</param>
public Task<TokenInformationEx> GenerateTokenInformationAsync(MailAccount account)
=> GetTokenInformationAsync(account);
public async Task<TokenInformationEx> GetTokenInformationAsync(MailAccount account)
{
var userCredential = await GetGoogleUserCredentialAsync(account);
if (userCredential.Token.IsStale)
{
await userCredential.RefreshTokenAsync(CancellationToken.None);
}
return new TokenInformationEx(userCredential.Token.AccessToken, account.Address);
}
private Task<UserCredential> GetGoogleUserCredentialAsync(MailAccount account)
{
return GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets()
{
ClientId = ClientId
}, AuthenticatorConfig.GetGmailScope(account?.IsMailAccessGranted != false, account?.IsCalendarAccessGranted == true), account.Id.ToString(), CancellationToken.None, new FileDataStore(AuthenticatorConfig.GmailTokenStoreIdentifier));
}
}
+152
View File
@@ -0,0 +1,152 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Broker;
using Microsoft.Identity.Client.Extensions.Msal;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Authentication;
namespace Wino.Authentication;
public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
{
private const string TokenCacheFileName = "OutlookCache.bin";
private bool isTokenCacheAttached = false;
// Outlook
private const string Authority = "https://login.microsoftonline.com/common";
public override MailProviderType ProviderType => MailProviderType.Outlook;
private readonly IPublicClientApplication _publicClientApplication;
private readonly INativeAppService _nativeAppService;
private readonly IApplicationConfiguration _applicationConfiguration;
public OutlookAuthenticator(INativeAppService nativeAppService,
IApplicationConfiguration applicationConfiguration,
IAuthenticatorConfig authenticatorConfig) : base(authenticatorConfig)
{
_nativeAppService = nativeAppService;
_applicationConfiguration = applicationConfiguration;
var authenticationRedirectUri = nativeAppService.GetWebAuthenticationBrokerUri();
var options = new BrokerOptions(BrokerOptions.OperatingSystems.Windows)
{
Title = "Wino Mail",
ListOperatingSystemAccounts = true,
};
PublicClientApplicationBuilder outlookAppBuilder = null;
// Being created from an app notification.
// This is where we avoid all interactive shit for authentication.
if (nativeAppService.GetCoreWindowHwnd == null)
{
outlookAppBuilder = PublicClientApplicationBuilder.Create(AuthenticatorConfig.OutlookAuthenticatorClientId)
.WithDefaultRedirectUri()
.WithBroker(options)
.WithAuthority(Authority);
}
else
{
outlookAppBuilder = PublicClientApplicationBuilder.Create(AuthenticatorConfig.OutlookAuthenticatorClientId)
.WithBroker(options)
.WithParentActivityOrWindow(_nativeAppService.GetCoreWindowHwnd)
.WithDefaultRedirectUri()
.WithAuthority(Authority);
}
_publicClientApplication = outlookAppBuilder.Build();
}
private string[] GetScope(MailAccount account)
=> AuthenticatorConfig.GetOutlookScope(
account?.IsMailAccessGranted != false,
account?.IsCalendarAccessGranted == true);
private async Task EnsureTokenCacheAttachedAsync()
{
if (!isTokenCacheAttached)
{
var storageProperties = new StorageCreationPropertiesBuilder(TokenCacheFileName, _applicationConfiguration.PublisherSharedFolderPath).Build();
var msalcachehelper = await MsalCacheHelper.CreateAsync(storageProperties);
msalcachehelper.RegisterCache(_publicClientApplication.UserTokenCache);
isTokenCacheAttached = true;
}
}
public async Task<TokenInformationEx> GetTokenInformationAsync(MailAccount account)
{
await EnsureTokenCacheAttachedAsync();
var storedAccount = (await _publicClientApplication.GetAccountsAsync()).FirstOrDefault(
a => string.Equals(a.Username?.Trim(), account.Address?.Trim(), StringComparison.OrdinalIgnoreCase));
if (storedAccount == null)
return await GenerateTokenInformationAsync(account);
try
{
var authResult = await _publicClientApplication.AcquireTokenSilent(GetScope(account), storedAccount).ExecuteAsync();
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
}
catch (MsalUiRequiredException)
{
// Somehow MSAL is not able to refresh the token silently.
// Force interactive login which will include calendar scopes.
// The calling code should update account.IsCalendarAccessGranted = true after successful authentication.
return await GenerateTokenInformationAsync(account);
}
catch (Exception)
{
throw;
}
}
public async Task<TokenInformationEx> GenerateTokenInformationAsync(MailAccount account)
{
try
{
await EnsureTokenCacheAttachedAsync();
// Interactive authentication required but window doesn't exist.
// This can happen when being called from a notification background task and the token is expired.
// Force account attention;
if (_nativeAppService.GetCoreWindowHwnd == null) throw new AuthenticationAttentionException(account);
AuthenticationResult authResult = await _publicClientApplication
.AcquireTokenInteractive(GetScope(account))
.ExecuteAsync();
// If the account is null, it means it's the initial creation of it.
// If not, make sure the authenticated user address matches the username.
// When people refresh their token, accounts must match.
if (account?.Address != null && !account.Address.Equals(authResult.Account.Username, StringComparison.OrdinalIgnoreCase))
{
throw new AuthenticationException("Authenticated address does not match with your account address. If you are signing with a Office365, it is not officially supported yet.");
}
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
}
catch (MsalClientException msalClientException)
{
if (msalClientException.ErrorCode == "authentication_canceled" || msalClientException.ErrorCode == "access_denied")
throw new AccountSetupCanceledException();
throw;
}
throw new AuthenticationException(Translator.Exception_UnknowErrorDuringAuthentication, new Exception(Translator.Exception_TokenGenerationFailed));
}
}
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<RootNamespace>Wino.Authentication</RootNamespace>
<Platforms>x86;x64;arm64</Platforms>
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<IsTrimmable>true</IsTrimmable>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Diagnostics" />
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="Google.Apis.Auth" />
<PackageReference Include="Microsoft.Identity.Client" />
<PackageReference Include="Microsoft.Identity.Client.Broker" />
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" />
<PackageReference Include="Sentry.Serilog" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
</ItemGroup>
</Project>
-34
View File
@@ -1,34 +0,0 @@
using Microsoft.Toolkit.Uwp.Notifications;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Background;
namespace Wino.BackgroundTasks
{
/// <summary>
/// Creates a toast notification to notify user when the Store update happens.
/// </summary>
public sealed class AppUpdatedTask : IBackgroundTask
{
public void Run(IBackgroundTaskInstance taskInstance)
{
var def = taskInstance.GetDeferral();
var builder = new ToastContentBuilder();
builder.SetToastScenario(ToastScenario.Default);
Package package = Package.Current;
PackageId packageId = package.Id;
PackageVersion version = packageId.Version;
var versionText = string.Format("{0}.{1}.{2}.{3}", version.Major, version.Minor, version.Build, version.Revision);
// TODO: Handle with Translator, but it's not initialized here yet.
builder.AddText("Wino Mail is updated!");
builder.AddText(string.Format("New version {0} is ready.", versionText));
builder.Show();
def.Complete();
}
}
}
@@ -1,29 +0,0 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Wino.BackgroundTasks")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Wino.BackgroundTasks")]
[assembly: AssemblyCopyright("Copyright © 2023")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: ComVisible(false)]
@@ -1,19 +0,0 @@
using System;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Background;
namespace Wino.BackgroundTasks
{
public sealed class SessionConnectedTask : IBackgroundTask
{
public async void Run(IBackgroundTaskInstance taskInstance)
{
var def = taskInstance.GetDeferral();
// Run server on session connected by launching the Full Thrust process.
await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
def.Complete();
}
}
}
@@ -1,147 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{D9EF0F59-F5F2-4D6C-A5BA-84043D8F3E08}</ProjectGuid>
<OutputType>winmdobj</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Wino.BackgroundTasks</RootNamespace>
<AssemblyName>Wino.BackgroundTasks</AssemblyName>
<DefaultLanguage>en-US</DefaultLanguage>
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
<TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.22621.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<MinimumVisualStudioVersion>14</MinimumVisualStudioVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<AllowCrossPlatformRetargeting>false</AllowCrossPlatformRetargeting>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<PlatformTarget>x86</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x86\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<PlatformTarget>x86</PlatformTarget>
<OutputPath>bin\x86\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM'">
<PlatformTarget>ARM</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\ARM\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM'">
<PlatformTarget>ARM</PlatformTarget>
<OutputPath>bin\ARM\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM64'">
<PlatformTarget>ARM64</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\ARM64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM64'">
<PlatformTarget>ARM64</PlatformTarget>
<OutputPath>bin\ARM64\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<PlatformTarget>x64</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<PlatformTarget>x64</PlatformTarget>
<OutputPath>bin\x64\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
</PropertyGroup>
<ItemGroup>
<Compile Include="AppUpdatedTask.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SessionConnectedTask.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
<Version>6.2.14</Version>
</PackageReference>
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications">
<Version>7.1.3</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj">
<Project>{CF3312E5-5DA0-4867-9945-49EA7598AF1F}</Project>
<Name>Wino.Core.Domain</Name>
</ProjectReference>
<ProjectReference Include="..\Wino.Core.UWP\Wino.Core.UWP.csproj">
<Project>{395f19ba-1e42-495c-9db5-1a6f537fccb8}</Project>
<Name>Wino.Core.UWP</Name>
</ProjectReference>
<ProjectReference Include="..\Wino.Core\Wino.Core.csproj">
<Project>{e6b1632a-8901-41e8-9ddf-6793c7698b0b}</Project>
<Name>Wino.Core</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<SDKReference Include="WindowsDesktop, Version=10.0.22621.0">
<Name>Windows Desktop Extensions for the UWP</Name>
</SDKReference>
</ItemGroup>
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '14.0' ">
<VisualStudioVersion>14.0</VisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>
@@ -0,0 +1,123 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels;
using Wino.Messaging.Client.Calendar;
namespace Wino.Calendar.ViewModels;
/// <summary>
/// ViewModel for managing calendar account settings.
/// </summary>
public partial class CalendarAccountSettingsPageViewModel : CalendarBaseViewModel
{
private readonly ICalendarService _calendarService;
private readonly IAccountService _accountService;
[ObservableProperty]
public partial MailAccount Account { get; set; }
[ObservableProperty]
public partial AccountCalendar AccountCalendar { get; set; }
[ObservableProperty]
public partial string AccountColorHex { get; set; } = "#0078D4";
[ObservableProperty]
public partial bool IsSyncEnabled { get; set; }
public ObservableCollection<ShowAsOption> ShowAsOptions { get; } = new ObservableCollection<ShowAsOption>();
[ObservableProperty]
public partial ShowAsOption SelectedDefaultShowAsOption { get; set; }
public CalendarAccountSettingsPageViewModel(ICalendarService calendarService, IAccountService accountService)
{
_calendarService = calendarService;
_accountService = accountService;
// Initialize ShowAs options
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Free));
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Tentative));
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Busy));
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.OutOfOffice));
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.WorkingElsewhere));
}
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
if (parameters is AccountCalendar selectedCalendar)
{
Account = await _accountService.GetAccountAsync(selectedCalendar.AccountId);
AccountCalendar = await _calendarService.GetAccountCalendarAsync(selectedCalendar.Id) ?? selectedCalendar;
}
else if (parameters is Guid accountId)
{
Account = await _accountService.GetAccountAsync(accountId);
var calendars = await _calendarService.GetAccountCalendarsAsync(accountId);
AccountCalendar = calendars.FirstOrDefault(c => c.IsPrimary) ?? calendars.FirstOrDefault();
}
else
{
return;
}
if (Account == null || AccountCalendar == null)
return;
// Initialize properties from AccountCalendar
AccountColorHex = AccountCalendar.BackgroundColorHex ?? "#0078D4";
IsSyncEnabled = AccountCalendar.IsSynchronizationEnabled;
SelectedDefaultShowAsOption = ShowAsOptions.FirstOrDefault(o => o.ShowAs == AccountCalendar.DefaultShowAs) ?? ShowAsOptions[2];
}
partial void OnAccountColorHexChanged(string value)
{
if (AccountCalendar != null && !string.IsNullOrEmpty(value))
{
AccountCalendar.BackgroundColorHex = value;
AccountCalendar.IsBackgroundColorUserOverridden = true;
SaveChangesAsync();
}
}
partial void OnIsSyncEnabledChanged(bool value)
{
if (AccountCalendar != null)
{
AccountCalendar.IsSynchronizationEnabled = value;
SaveChangesAsync();
}
}
partial void OnSelectedDefaultShowAsOptionChanged(ShowAsOption value)
{
if (AccountCalendar != null && value != null)
{
AccountCalendar.DefaultShowAs = value.ShowAs;
SaveChangesAsync();
}
}
private async void SaveChangesAsync()
{
if (AccountCalendar == null)
return;
await _calendarService.UpdateAccountCalendarAsync(AccountCalendar);
// Send message to update UI
Messenger.Send(new CalendarListUpdated(AccountCalendar));
}
}
@@ -0,0 +1,562 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Serilog;
using Wino.Calendar.ViewModels.Data;
using Wino.Calendar.ViewModels.Interfaces;
using Wino.Core.Domain;
using Wino.Core.Domain.Collections;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.MenuItems;
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.ViewModels;
using Wino.Messaging.Client.Calendar;
using Wino.Messaging.Server;
using Wino.Messaging.UI;
namespace Wino.Calendar.ViewModels;
public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
ICalendarShellClient,
IRecipient<CalendarDisplayTypeChangedMessage>,
IRecipient<AccountRemovedMessage>
{
public IPreferencesService PreferencesService { get; }
public IStatePersistanceService StatePersistenceService { get; }
public IAccountCalendarStateService AccountCalendarStateService { get; }
public INavigationService NavigationService { get; }
public WinoApplicationMode Mode => WinoApplicationMode.Calendar;
public bool HandlesNavigationSelection => false;
public VisibleDateRange CurrentVisibleRange => _calendarPageViewModel.CurrentVisibleRange;
public string VisibleDateRangeText => _calendarPageViewModel.VisibleDateRangeText;
System.Collections.IEnumerable ICalendarShellClient.GroupedAccountCalendars => AccountCalendarStateService.GroupedAccountCalendars;
System.Collections.IEnumerable ICalendarShellClient.DateNavigationHeaderItems => DateNavigationHeaderItems;
object IShellClient.SelectedMenuItem
{
get => null;
set { }
}
System.Windows.Input.ICommand ICalendarShellClient.TodayClickedCommand => TodayClickedCommand;
System.Windows.Input.ICommand ICalendarShellClient.DateClickedCommand => DateClickedCommand;
System.Windows.Input.ICommand ICalendarShellClient.PreviousDateRangeCommand => PreviousDateRangeCommand;
System.Windows.Input.ICommand ICalendarShellClient.NextDateRangeCommand => NextDateRangeCommand;
System.Windows.Input.ICommand ICalendarShellClient.SyncCommand => SyncCommand;
public bool CanSynchronizeCalendars => !AccountCalendarStateService.IsAnySynchronizationInProgress;
public MenuItemCollection MenuItems { get; private set; }
public MenuItemCollection FooterItems { get; private set; }
[ObservableProperty]
private int _selectedMenuItemIndex = -1;
[ObservableProperty]
private ObservableRangeCollection<string> dateNavigationHeaderItems = [];
[ObservableProperty]
private int _selectedDateNavigationHeaderIndex;
public bool IsVerticalCalendar => StatePersistenceService.CalendarDisplayType == CalendarDisplayType.Month;
[ObservableProperty]
private bool isStoreUpdateItemVisible;
private readonly SettingsItem _settingsItem = new();
private readonly StoreUpdateMenuItem _storeUpdateMenuItem = new();
private readonly SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1);
private readonly CalendarPageViewModel _calendarPageViewModel;
private readonly IMailDialogService _dialogService;
private readonly IStoreUpdateService _storeUpdateService;
private readonly IAccountService _accountService;
private readonly ICalendarService _calendarService;
private readonly IDateContextProvider _dateContextProvider;
private bool _runtimeSubscriptionsAttached;
private bool _hasRegisteredPersistentRecipients;
private DateTime? _navigationDate;
public CalendarAppShellViewModel(
IPreferencesService preferencesService,
IStatePersistanceService statePersistanceService,
IAccountService accountService,
ICalendarService calendarService,
IAccountCalendarStateService accountCalendarStateService,
INavigationService navigationService,
CalendarPageViewModel calendarPageViewModel,
IMailDialogService dialogService,
IStoreUpdateService storeUpdateService,
IDateContextProvider dateContextProvider)
{
PreferencesService = preferencesService;
StatePersistenceService = statePersistanceService;
AccountCalendarStateService = accountCalendarStateService;
NavigationService = navigationService;
_accountService = accountService;
_calendarService = calendarService;
_calendarPageViewModel = calendarPageViewModel;
_dialogService = dialogService;
_storeUpdateService = storeUpdateService;
_dateContextProvider = dateContextProvider;
_calendarPageViewModel.PropertyChanged += CalendarPageViewModelPropertyChanged;
AccountCalendarStateService.PropertyChanged += AccountCalendarStateServicePropertyChanged;
}
protected override void OnDispatcherAssigned()
{
base.OnDispatcherAssigned();
AccountCalendarStateService.Dispatcher = Dispatcher;
MenuItems = new MenuItemCollection(Dispatcher);
FooterItems = new MenuItemCollection(Dispatcher);
_ = RefreshFooterItemsAsync(false);
}
private void CalendarPageViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(CalendarPageViewModel.CurrentVisibleRange))
{
OnPropertyChanged(nameof(CurrentVisibleRange));
}
if (e.PropertyName == nameof(CalendarPageViewModel.CurrentVisibleRange) ||
e.PropertyName == nameof(CalendarPageViewModel.VisibleDateRangeText))
{
OnPropertyChanged(nameof(VisibleDateRangeText));
UpdateDateNavigationHeaderItems();
}
}
private void PrefefencesChanged(object sender, string e)
{
if (e != nameof(StatePersistenceService.CalendarDisplayType))
return;
Messenger.Send(new CalendarDisplayTypeChangedMessage(StatePersistenceService.CalendarDisplayType));
OnPropertyChanged(nameof(IsVerticalCalendar));
UpdateDateNavigationHeaderItems();
NavigateCalendarDate(GetDisplayTypeSwitchDate());
}
private async void PreferencesServiceChanged(object sender, string e)
{
if (e == nameof(IPreferencesService.IsStoreUpdateNotificationsEnabled))
{
await RefreshFooterItemsAsync(false);
}
}
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
{
if (!_hasRegisteredPersistentRecipients)
{
RegisterRecipients();
_hasRegisteredPersistentRecipients = true;
}
AttachRuntimeSubscriptions();
var activationContext = parameters as ShellModeActivationContext;
var shouldRunStartupFlows = activationContext?.IsInitialActivation ?? true;
var navigationArgs = activationContext?.Parameter as CalendarPageNavigationArgs;
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
PreferencesService.PreferenceChanged += PreferencesServiceChanged;
await RefreshFooterItemsAsync(mode == NavigationMode.New);
UpdateDateNavigationHeaderItems();
await InitializeAccountCalendarsAsync();
ValidateConfiguredNewEventCalendar();
if (navigationArgs != null)
{
NavigationService.Navigate(WinoPage.CalendarPage, navigationArgs);
}
else if (shouldRunStartupFlows || _calendarPageViewModel.CurrentVisibleRange == null)
{
TodayClicked();
}
}
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
{
DetachRuntimeSubscriptions();
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
_ = ExecuteUIThread(() =>
{
DateNavigationHeaderItems.Clear();
AccountCalendarStateService.ClearGroupedAccountCalendars();
SelectedDateNavigationHeaderIndex = -1;
});
_calendarPageViewModel.CleanupForShellDeactivation();
}
public void PrepareForShellShutdown()
{
DetachRuntimeSubscriptions();
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
if (_hasRegisteredPersistentRecipients)
{
UnregisterRecipients();
_hasRegisteredPersistentRecipients = false;
}
DateNavigationHeaderItems.Clear();
SelectedDateNavigationHeaderIndex = -1;
SelectedMenuItemIndex = -1;
MenuItems?.Clear();
FooterItems?.Clear();
AccountCalendarStateService.ClearGroupedAccountCalendars();
_calendarPageViewModel.CleanupForShellDeactivation();
}
private void AccountCalendarStateServicePropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(IAccountCalendarStateService.IsAnySynchronizationInProgress))
return;
OnPropertyChanged(nameof(CanSynchronizeCalendars));
SyncCommand.NotifyCanExecuteChanged();
}
private void AttachRuntimeSubscriptions()
{
if (_runtimeSubscriptionsAttached)
return;
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged;
StatePersistenceService.StatePropertyChanged += PrefefencesChanged;
_runtimeSubscriptionsAttached = true;
}
private void DetachRuntimeSubscriptions()
{
if (!_runtimeSubscriptionsAttached)
return;
AccountCalendarStateService.AccountCalendarSelectionStateChanged -= UpdateAccountCalendarRequested;
AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged -= AccountCalendarStateCollectivelyChanged;
StatePersistenceService.StatePropertyChanged -= PrefefencesChanged;
_runtimeSubscriptionsAttached = false;
}
private async Task RefreshFooterItemsAsync(bool showNotification)
{
await ExecuteUIThread(() =>
{
FooterItems.Clear();
});
}
private async Task StartStoreUpdateAsync()
{
await _storeUpdateService.StartUpdateAsync().ConfigureAwait(false);
await RefreshFooterItemsAsync(false).ConfigureAwait(false);
}
private async void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
{
try
{
await _accountCalendarUpdateSemaphoreSlim.WaitAsync();
foreach (var calendar in e.AccountCalendars)
{
await _calendarService.UpdateAccountCalendarAsync(calendar.AccountCalendar).ConfigureAwait(false);
}
}
catch (Exception ex)
{
Log.Error(ex, "Error while waiting for account calendar update semaphore.");
}
finally
{
_accountCalendarUpdateSemaphoreSlim.Release();
}
}
private async void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e)
=> await _calendarService.UpdateAccountCalendarAsync(e.AccountCalendar).ConfigureAwait(false);
private async Task InitializeAccountCalendarsAsync()
{
await Dispatcher.ExecuteOnUIThread(() => AccountCalendarStateService.ClearGroupedAccountCalendars());
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
foreach (var account in accounts)
{
if (!GroupedAccountCalendarViewModel.SupportsCalendar(account))
continue;
var accountCalendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
var calendarViewModels = accountCalendars.Select(calendar => new AccountCalendarViewModel(account, calendar)).ToList();
var groupedAccountCalendarViewModel = new GroupedAccountCalendarViewModel(account, calendarViewModels);
await Dispatcher.ExecuteOnUIThread(() =>
{
AccountCalendarStateService.AddGroupedAccountCalendar(groupedAccountCalendarViewModel);
});
}
}
private void NavigateCalendarDate(DateTime date)
{
_navigationDate = date.Date;
ForceNavigateCalendarDate();
}
private void ForceNavigateCalendarDate()
{
var args = new CalendarPageNavigationArgs
{
NavigationDate = _navigationDate ?? DateTime.Now.Date
};
NavigationService.Navigate(WinoPage.CalendarPage, args);
_navigationDate = null;
}
[RelayCommand(CanExecute = nameof(CanSynchronizeCalendars))]
private async Task Sync()
{
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
foreach (var account in accounts)
{
Messenger.Send(new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions
{
AccountId = account.Id,
Type = CalendarSynchronizationType.Strict
}));
}
}
private DateTime GetDisplayTypeSwitchDate()
{
var today = _dateContextProvider.GetToday();
var settings = PreferencesService.GetCurrentCalendarSettings();
var referenceRange = CurrentVisibleRange
?? CalendarRangeResolver.Resolve(new CalendarDisplayRequest(StatePersistenceService.CalendarDisplayType, today), settings, today);
var targetRange = CalendarRangeResolver.ChangeDisplayType(referenceRange, StatePersistenceService.CalendarDisplayType, settings, today);
return targetRange.AnchorDate.ToDateTime(TimeOnly.MinValue);
}
[RelayCommand]
private void TodayClicked()
{
NavigateCalendarDate(_dateContextProvider.GetToday().ToDateTime(TimeOnly.MinValue));
}
[RelayCommand]
private void PreviousDateRange()
{
NavigateRelativePeriod(-1);
}
[RelayCommand]
private void NextDateRange()
{
NavigateRelativePeriod(1);
}
private void NavigateRelativePeriod(int direction)
{
var today = _dateContextProvider.GetToday();
var settings = PreferencesService.GetCurrentCalendarSettings();
var referenceRange = CurrentVisibleRange
?? CalendarRangeResolver.Resolve(new CalendarDisplayRequest(StatePersistenceService.CalendarDisplayType, today), settings, today);
var targetRange = CalendarRangeResolver.Navigate(referenceRange, direction, settings, today);
NavigateCalendarDate(targetRange.AnchorDate.ToDateTime(TimeOnly.MinValue));
}
public async Task HandleNavigationItemInvokedAsync(IMenuItem menuItem)
{
switch (menuItem)
{
case NewMailMenuItem:
await NewEventAsync().ConfigureAwait(false);
break;
case SettingsItem:
NavigationService.Navigate(WinoPage.SettingsPage);
break;
case StoreUpdateMenuItem:
await StartStoreUpdateAsync().ConfigureAwait(false);
break;
}
}
[RelayCommand]
private async Task NewEventAsync()
{
var pickedCalendar = TryResolveConfiguredNewEventCalendar();
if (pickedCalendar == null)
{
var availableGroups = AccountCalendarStateService.GroupedAccountCalendars
.Where(group => group.AccountCalendars.Count > 0)
.Select(group => new CalendarPickerAccountGroup
{
Account = group.Account,
Calendars = group.AccountCalendars.Select(calendar => calendar.AccountCalendar).ToList()
})
.ToList();
if (availableGroups.Count == 0)
{
_dialogService.InfoBarMessage(
Translator.CalendarEventCompose_NoCalendarsTitle,
Translator.CalendarEventCompose_NoCalendarsMessage,
InfoBarMessageType.Warning);
return;
}
var pickingResult = await _dialogService.ShowSingleCalendarPickerDialogAsync(availableGroups);
if (pickingResult.ShouldNavigateToCalendarSettings)
{
NavigationService.Navigate(WinoPage.CalendarPreferenceSettingsPage);
return;
}
pickedCalendar = pickingResult.PickedCalendar;
}
if (pickedCalendar == null)
return;
var (startDate, endDate) = GetDefaultComposeDateRange();
NavigationService.Navigate(WinoPage.CalendarEventComposePage, new CalendarEventComposeNavigationArgs
{
SelectedCalendarId = pickedCalendar.Id,
StartDate = startDate,
EndDate = endDate
});
}
public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
{
if (args.Handled || args.Mode != WinoApplicationMode.Calendar)
return;
if (args.Action == KeyboardShortcutAction.NewEvent)
{
await NewEventAsync();
args.Handled = true;
}
}
[RelayCommand]
private void DateClicked(CalendarViewDayClickedEventArgs clickedDateArgs)
=> NavigateCalendarDate(clickedDateArgs.ClickedDate);
protected override void RegisterRecipients()
{
base.RegisterRecipients();
UnregisterRecipients();
Messenger.Register<CalendarDisplayTypeChangedMessage>(this);
Messenger.Register<AccountRemovedMessage>(this);
}
protected override void UnregisterRecipients()
{
base.UnregisterRecipients();
Messenger.Unregister<CalendarDisplayTypeChangedMessage>(this);
Messenger.Unregister<AccountRemovedMessage>(this);
}
private void UpdateDateNavigationHeaderItems()
{
var headerText = VisibleDateRangeText;
DateNavigationHeaderItems.ReplaceRange(string.IsNullOrWhiteSpace(headerText) ? [] : [headerText]);
SelectedDateNavigationHeaderIndex = DateNavigationHeaderItems.Count > 0 ? 0 : -1;
}
public void Receive(CalendarDisplayTypeChangedMessage message)
{
OnPropertyChanged(nameof(IsVerticalCalendar));
UpdateDateNavigationHeaderItems();
}
public async void Receive(AccountRemovedMessage message)
{
await InitializeAccountCalendarsAsync();
ValidateConfiguredNewEventCalendar();
}
private AccountCalendar TryResolveConfiguredNewEventCalendar()
{
ValidateConfiguredNewEventCalendar();
if (PreferencesService.NewEventButtonBehavior != NewEventButtonBehavior.AlwaysUseSpecificCalendar
|| !PreferencesService.DefaultNewEventCalendarId.HasValue)
{
return null;
}
return AccountCalendarStateService.AllCalendars
.FirstOrDefault(calendar => calendar.Id == PreferencesService.DefaultNewEventCalendarId.Value)?
.AccountCalendar;
}
private void ValidateConfiguredNewEventCalendar()
{
if (PreferencesService.NewEventButtonBehavior != NewEventButtonBehavior.AlwaysUseSpecificCalendar
|| !PreferencesService.DefaultNewEventCalendarId.HasValue)
{
return;
}
var exists = AccountCalendarStateService.AllCalendars
.Any(calendar => calendar.Id == PreferencesService.DefaultNewEventCalendarId.Value);
if (!exists)
{
PreferencesService.NewEventButtonBehavior = NewEventButtonBehavior.AskEachTime;
PreferencesService.DefaultNewEventCalendarId = null;
}
}
private static (DateTime StartDate, DateTime EndDate) GetDefaultComposeDateRange()
{
var localNow = DateTime.Now;
var roundedMinutes = localNow.Minute switch
{
< 30 => 30,
30 when localNow.Second == 0 && localNow.Millisecond == 0 => 30,
_ => 60
};
var startDate = new DateTime(localNow.Year, localNow.Month, localNow.Day, localNow.Hour, 0, 0);
startDate = roundedMinutes == 60 ? startDate.AddHours(1) : startDate.AddMinutes(roundedMinutes);
return (startDate, startDate.AddMinutes(30));
}
void IShellClient.Activate(ShellModeActivationContext activationContext)
=> OnNavigatedTo(NavigationMode.New, activationContext);
void IShellClient.Deactivate()
=> OnNavigatedFrom(NavigationMode.New, null!);
Task IShellClient.HandleNavigationItemInvokedAsync(IMenuItem menuItem)
=> menuItem == null ? Task.CompletedTask : HandleNavigationItemInvokedAsync(menuItem);
Task IShellClient.HandleNavigationSelectionChangedAsync(IMenuItem menuItem)
=> Task.CompletedTask;
}
@@ -0,0 +1,763 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using EmailValidation;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Validation;
using Wino.Core.ViewModels;
namespace Wino.Calendar.ViewModels;
public partial class CalendarEventComposePageViewModel : CalendarBaseViewModel
{
private readonly IAccountService _accountService;
private readonly ICalendarService _calendarService;
private readonly INavigationService _navigationService;
private readonly IMailDialogService _dialogService;
private readonly IContactService _contactService;
private readonly IPreferencesService _preferencesService;
private readonly IUnderlyingThemeService _underlyingThemeService;
private readonly IWinoRequestDelegator _winoRequestDelegator;
private readonly CalendarEventComposeResultValidator _composeResultValidator = new();
public Func<Task<string>> GetHtmlNotesAsync { get; set; }
public ObservableCollection<AccountCalendarViewModel> AvailableCalendars { get; } = [];
public ObservableCollection<GroupedAccountCalendarViewModel> AvailableCalendarGroups { get; } = [];
public ObservableCollection<CalendarComposeAttendeeViewModel> Attendees { get; } = [];
public ObservableCollection<CalendarComposeAttachmentViewModel> Attachments { get; } = [];
public ObservableCollection<ShowAsOption> ShowAsOptions { get; } = [];
public ObservableCollection<ReminderOption> ReminderOptions { get; } = [];
public ObservableCollection<int> RecurrenceIntervalOptions { get; } = [];
public ObservableCollection<CalendarComposeFrequencyOption> RecurrenceFrequencyOptions { get; } = [];
public ObservableCollection<CalendarComposeWeekdayOption> WeekdayOptions { get; } = [];
[ObservableProperty]
public partial AccountCalendarViewModel SelectedCalendar { get; set; }
[ObservableProperty]
public partial string Title { get; set; } = string.Empty;
[ObservableProperty]
public partial string Location { get; set; } = string.Empty;
[ObservableProperty]
public partial bool IsAllDay { get; set; }
[ObservableProperty]
public partial DateTimeOffset StartDate { get; set; }
[ObservableProperty]
public partial TimeSpan StartTime { get; set; }
[ObservableProperty]
public partial TimeSpan EndTime { get; set; }
[ObservableProperty]
public partial DateTimeOffset AllDayEndDate { get; set; }
[ObservableProperty]
public partial bool IsRecurring { get; set; }
[ObservableProperty]
public partial int SelectedRecurrenceInterval { get; set; } = 1;
[ObservableProperty]
public partial CalendarComposeFrequencyOption SelectedRecurrenceFrequencyOption { get; set; }
[ObservableProperty]
public partial DateTimeOffset? RecurrenceEndDate { get; set; }
[ObservableProperty]
public partial string RecurrenceSummary { get; set; } = string.Empty;
[ObservableProperty]
public partial ReminderOption SelectedReminderOption { get; set; }
[ObservableProperty]
public partial ShowAsOption SelectedShowAsOption { get; set; }
[ObservableProperty]
public partial bool IsDarkWebviewRenderer { get; set; }
[ObservableProperty]
public partial CalendarEventComposeResult LastCreatedResult { get; set; }
public CalendarSettings CurrentSettings { get; }
public string TimePickerClockIdentifier => CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour ? "24HourClock" : "12HourClock";
public bool HasAttachments => Attachments.Count > 0;
public bool IsSelectedCalendarCalDav => SelectedCalendar?.Account?.ProviderType == MailProviderType.IMAP4 &&
SelectedCalendar.Account.ServerInformation?.CalendarSupportMode == ImapCalendarSupportMode.CalDav;
public bool CanAddAttachments => !IsSelectedCalendarCalDav;
public string AttachmentsDisabledTooltipText => IsSelectedCalendarCalDav
? Translator.CalendarEventCompose_AttachmentsNotSupportedForCalDav
: string.Empty;
public string SelectedCalendarDisplayText => SelectedCalendar?.Name ?? Translator.CalendarEventCompose_SelectCalendar;
public string SelectedCalendarAccountText => SelectedCalendar?.Account?.Address ?? string.Empty;
public bool IsDailyRecurrenceSelected => SelectedRecurrenceFrequencyOption?.Frequency == CalendarItemRecurrenceFrequency.Daily;
public CalendarEventComposePageViewModel(IAccountService accountService,
ICalendarService calendarService,
INavigationService navigationService,
IMailDialogService dialogService,
IContactService contactService,
IPreferencesService preferencesService,
IUnderlyingThemeService underlyingThemeService,
IWinoRequestDelegator winoRequestDelegator)
{
_accountService = accountService;
_calendarService = calendarService;
_navigationService = navigationService;
_dialogService = dialogService;
_contactService = contactService;
_preferencesService = preferencesService;
_underlyingThemeService = underlyingThemeService;
_winoRequestDelegator = winoRequestDelegator;
CurrentSettings = _preferencesService.GetCurrentCalendarSettings();
IsDarkWebviewRenderer = _underlyingThemeService.IsUnderlyingThemeDark();
Attachments.CollectionChanged += AttachmentsCollectionChanged;
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Free));
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Tentative));
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.Busy));
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.OutOfOffice));
ShowAsOptions.Add(new ShowAsOption(CalendarItemShowAs.WorkingElsewhere));
foreach (var reminderMinutes in _calendarService.GetPredefinedReminderMinutes().OrderByDescending(x => x))
{
ReminderOptions.Add(new ReminderOption(reminderMinutes));
}
foreach (var interval in Enumerable.Range(1, 99))
{
RecurrenceIntervalOptions.Add(interval);
}
RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Daily, Translator.CalendarEventCompose_FrequencyDay));
RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Weekly, Translator.CalendarEventCompose_FrequencyWeek));
RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Monthly, Translator.CalendarEventCompose_FrequencyMonth));
RecurrenceFrequencyOptions.Add(new CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency.Yearly, Translator.CalendarEventCompose_FrequencyYear));
SelectedRecurrenceFrequencyOption = RecurrenceFrequencyOptions.FirstOrDefault();
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Monday, "MO", Translator.CalendarEventCompose_Weekday_Monday));
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Tuesday, "TU", Translator.CalendarEventCompose_Weekday_Tuesday));
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Wednesday, "WE", Translator.CalendarEventCompose_Weekday_Wednesday));
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Thursday, "TH", Translator.CalendarEventCompose_Weekday_Thursday));
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Friday, "FR", Translator.CalendarEventCompose_Weekday_Friday));
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Saturday, "SA", Translator.CalendarEventCompose_Weekday_Saturday));
WeekdayOptions.Add(CreateWeekdayOption(DayOfWeek.Sunday, "SU", Translator.CalendarEventCompose_Weekday_Sunday));
SelectedReminderOption = GetDefaultReminderOption();
SelectedShowAsOption = ShowAsOptions.FirstOrDefault(option => option.ShowAs == CalendarItemShowAs.Busy);
var (defaultStart, defaultEnd) = GetDefaultComposeDateRange();
ApplyDateRange(defaultStart, defaultEnd, false);
}
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
await LoadAvailableCalendarsAsync();
var args = parameters as CalendarEventComposeNavigationArgs;
ApplyNavigationArgs(args);
UpdateRecurrenceSummary();
}
partial void OnSelectedCalendarChanged(AccountCalendarViewModel value)
{
if (value == null)
return;
SelectedShowAsOption = ShowAsOptions.FirstOrDefault(option => option.ShowAs == value.DefaultShowAs)
?? ShowAsOptions.FirstOrDefault();
if (IsSelectedCalendarCalDav && Attachments.Count > 0)
{
Attachments.Clear();
}
OnPropertyChanged(nameof(IsSelectedCalendarCalDav));
OnPropertyChanged(nameof(CanAddAttachments));
OnPropertyChanged(nameof(AttachmentsDisabledTooltipText));
OnPropertyChanged(nameof(SelectedCalendarDisplayText));
OnPropertyChanged(nameof(SelectedCalendarAccountText));
}
partial void OnIsAllDayChanged(bool value)
{
if (value)
{
if (AllDayEndDate.Date <= StartDate.Date)
{
AllDayEndDate = StartDate.AddDays(1);
}
}
UpdateRecurrenceSummary();
}
partial void OnStartDateChanged(DateTimeOffset value)
{
if (IsAllDay && AllDayEndDate.Date <= value.Date)
{
AllDayEndDate = value.AddDays(1);
}
if (IsRecurring && WeekdayOptions.All(option => !option.IsSelected))
{
SelectSingleWeekday(value.DayOfWeek);
}
UpdateRecurrenceSummary();
}
partial void OnStartTimeChanged(TimeSpan value) => UpdateRecurrenceSummary();
partial void OnEndTimeChanged(TimeSpan value) => UpdateRecurrenceSummary();
partial void OnAllDayEndDateChanged(DateTimeOffset value) => UpdateRecurrenceSummary();
partial void OnIsRecurringChanged(bool value)
{
if (value && WeekdayOptions.All(option => !option.IsSelected))
{
SelectSingleWeekday(StartDate.DayOfWeek);
}
UpdateRecurrenceSummary();
}
partial void OnSelectedRecurrenceIntervalChanged(int value) => UpdateRecurrenceSummary();
partial void OnSelectedRecurrenceFrequencyOptionChanged(CalendarComposeFrequencyOption value)
{
OnPropertyChanged(nameof(IsDailyRecurrenceSelected));
UpdateRecurrenceSummary();
}
partial void OnRecurrenceEndDateChanged(DateTimeOffset? value) => UpdateRecurrenceSummary();
[RelayCommand]
private async Task AddAttachmentsAsync()
{
if (!CanAddAttachments)
return;
var pickedFiles = await _dialogService.PickFilesMetadataAsync("*");
if (pickedFiles.Count == 0)
return;
await ExecuteUIThread(() =>
{
foreach (var file in pickedFiles)
{
TryAddAttachment(file.FileName, file.FullFilePath, file.FileExtension, file.Size);
}
});
}
public bool TryAddAttachment(string filePath, long size)
{
if (string.IsNullOrWhiteSpace(filePath))
return false;
var fileName = Path.GetFileName(filePath);
var fileExtension = Path.GetExtension(filePath);
return TryAddAttachment(fileName, filePath, fileExtension, size);
}
[RelayCommand]
private void RemoveAttachment(CalendarComposeAttachmentViewModel attachment)
{
if (attachment == null)
return;
Attachments.Remove(attachment);
}
[RelayCommand]
private void ClearRecurrenceEndDate()
{
RecurrenceEndDate = null;
}
[RelayCommand]
private void Cancel()
{
_navigationService.GoBack();
}
[RelayCommand]
private async Task CreateAsync()
{
var uniqueAttendees = Attendees
.GroupBy(attendee => attendee.Email, StringComparer.OrdinalIgnoreCase)
.Select(group => group.First())
.ToList();
var createdResult = await BuildResultAsync(uniqueAttendees);
try
{
_composeResultValidator.Validate(createdResult);
}
catch (CalendarEventComposeValidationException ex)
{
ShowValidationMessage(ex.Message);
return;
}
LastCreatedResult = createdResult;
await _winoRequestDelegator.ExecuteAsync(new CalendarOperationPreparationRequest(
CalendarSynchronizerOperation.CreateEvent,
ComposeResult: createdResult));
NavigateBackToCalendar(createdResult.StartDate);
}
private void NavigateBackToCalendar(DateTime targetDate)
{
_navigationService.Navigate(
WinoPage.CalendarPage,
new CalendarPageNavigationArgs
{
NavigationDate = targetDate,
ForceReload = true
});
}
public async Task<List<AccountContact>> SearchContactsAsync(string queryText)
{
if (string.IsNullOrWhiteSpace(queryText) || queryText.Length < 2)
return [];
return await _contactService.GetAddressInformationAsync(queryText).ConfigureAwait(false);
}
public async Task<CalendarComposeAttendeeViewModel> GetAttendeeAsync(string tokenText)
{
if (!EmailValidator.Validate(tokenText))
return null;
var existing = Attendees.Any(attendee => attendee.Email.Equals(tokenText, StringComparison.OrdinalIgnoreCase));
if (existing)
return null;
var info = await _contactService.GetAddressInformationByAddressAsync(tokenText).ConfigureAwait(false);
if (info != null)
{
return CalendarComposeAttendeeViewModel.FromContact(info);
}
return new CalendarComposeAttendeeViewModel(string.Empty, tokenText);
}
public void AddAttendee(CalendarComposeAttendeeViewModel attendee)
{
if (Attendees.Any(existing => existing.Email.Equals(attendee.Email, StringComparison.OrdinalIgnoreCase)))
return;
Attendees.Add(attendee);
}
[RelayCommand]
private void RemoveAttendee(CalendarComposeAttendeeViewModel attendee)
{
if (attendee == null)
return;
Attendees.Remove(attendee);
}
public void NotifyAddressExists()
{
_dialogService.InfoBarMessage(
Translator.Info_ContactExistsTitle,
Translator.Info_ContactExistsMessage,
InfoBarMessageType.Warning);
}
public void NotifyInvalidEmail(string address)
{
_dialogService.InfoBarMessage(
Translator.Info_InvalidAddressTitle,
string.Format(Translator.Info_InvalidAddressMessage, address),
InfoBarMessageType.Warning);
}
private async Task LoadAvailableCalendarsAsync()
{
var accountCalendars = new List<AccountCalendarViewModel>();
var groupedCalendars = new List<GroupedAccountCalendarViewModel>();
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
foreach (var account in accounts)
{
if (!GroupedAccountCalendarViewModel.SupportsCalendar(account))
continue;
var calendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
var viewModels = calendars
.Select(calendar => new AccountCalendarViewModel(account, calendar))
.ToList();
accountCalendars.AddRange(viewModels);
if (viewModels.Count > 0)
{
groupedCalendars.Add(new GroupedAccountCalendarViewModel(account, viewModels));
}
}
await ExecuteUIThread(() =>
{
AvailableCalendars.Clear();
AvailableCalendarGroups.Clear();
foreach (var calendar in accountCalendars.OrderBy(calendar => calendar.Account.Name).ThenBy(calendar => calendar.Name))
{
AvailableCalendars.Add(calendar);
}
foreach (var group in groupedCalendars.OrderBy(group => group.Account.Name))
{
AvailableCalendarGroups.Add(group);
}
});
}
private void ApplyNavigationArgs(CalendarEventComposeNavigationArgs args)
{
var (defaultStart, defaultEnd) = GetDefaultComposeDateRange();
var startDate = args?.StartDate != default ? args!.StartDate : defaultStart;
var endDate = args?.EndDate != default ? args!.EndDate : defaultEnd;
var isAllDay = args?.IsAllDay ?? false;
Title = args?.Title ?? string.Empty;
Location = args?.Location ?? string.Empty;
ApplyDateRange(startDate, endDate, isAllDay);
SelectedCalendar = ResolveSelectedCalendar(args?.SelectedCalendarId);
if (SelectedCalendar != null)
{
SelectedShowAsOption = ShowAsOptions.FirstOrDefault(option => option.ShowAs == SelectedCalendar.DefaultShowAs)
?? SelectedShowAsOption
?? ShowAsOptions.FirstOrDefault();
}
}
private AccountCalendarViewModel ResolveSelectedCalendar(Guid? selectedCalendarId)
{
if (selectedCalendarId.HasValue)
{
var selectedCalendar = AvailableCalendars.FirstOrDefault(calendar => calendar.Id == selectedCalendarId.Value);
if (selectedCalendar != null)
return selectedCalendar;
}
return AvailableCalendars.FirstOrDefault(calendar => calendar.IsPrimary) ?? AvailableCalendars.FirstOrDefault();
}
private void ApplyDateRange(DateTime startDate, DateTime endDate, bool isAllDay)
{
IsAllDay = isAllDay;
StartDate = new DateTimeOffset(startDate.Date);
StartTime = startDate.TimeOfDay;
EndTime = endDate.TimeOfDay;
AllDayEndDate = new DateTimeOffset((isAllDay ? endDate.Date : startDate.Date.AddDays(1)));
}
private async Task<CalendarEventComposeResult> BuildResultAsync(List<CalendarComposeAttendeeViewModel> uniqueAttendees)
{
if (RecurrenceEndDate.HasValue && RecurrenceEndDate.Value.Date < StartDate.Date)
{
throw new CalendarEventComposeValidationException(Translator.CalendarEventCompose_ValidationInvalidRecurrenceEnd);
}
var htmlNotes = GetHtmlNotesAsync == null ? string.Empty : await GetHtmlNotesAsync();
var effectiveStart = GetEffectiveStartDateTime();
var effectiveEnd = GetEffectiveEndDateTime();
return new CalendarEventComposeResult
{
CalendarId = SelectedCalendar?.Id ?? Guid.Empty,
AccountId = SelectedCalendar?.Account.Id ?? Guid.Empty,
Title = Title.Trim(),
Location = Location?.Trim() ?? string.Empty,
HtmlNotes = htmlNotes,
StartDate = effectiveStart,
EndDate = effectiveEnd,
IsAllDay = IsAllDay,
TimeZoneId = TimeZoneInfo.Local.Id,
ShowAs = SelectedShowAsOption?.ShowAs ?? SelectedCalendar?.DefaultShowAs ?? CalendarItemShowAs.Busy,
SelectedReminders = BuildSelectedReminders(),
Attendees = BuildAttendees(uniqueAttendees),
Attachments = CanAddAttachments
? Attachments.Select(attachment => attachment.ToDraftModel()).ToList()
: [],
Recurrence = BuildRecurrenceRule(),
RecurrenceSummary = RecurrenceSummary
};
}
private List<Reminder> BuildSelectedReminders()
{
if (SelectedReminderOption == null)
return [];
return
[
new Reminder
{
Id = Guid.NewGuid(),
CalendarItemId = Guid.Empty,
DurationInSeconds = SelectedReminderOption.Minutes * 60L,
ReminderType = CalendarItemReminderType.Popup
}
];
}
private static List<CalendarEventAttendee> BuildAttendees(IEnumerable<CalendarComposeAttendeeViewModel> attendees)
{
return attendees
.Select(attendee => new CalendarEventAttendee
{
Id = Guid.NewGuid(),
CalendarItemId = Guid.Empty,
Name = attendee.HasDistinctDisplayName ? attendee.DisplayName : string.Empty,
Email = attendee.Email,
AttendenceStatus = AttendeeStatus.NeedsAction,
IsOrganizer = false,
ResolvedContact = attendee.ResolvedContact
})
.ToList();
}
private ReminderOption GetDefaultReminderOption()
{
var reminderMinutes = Math.Max(1, _preferencesService.DefaultReminderDurationInSeconds / 60);
return ReminderOptions.FirstOrDefault(option => option.Minutes == reminderMinutes)
?? ReminderOptions.FirstOrDefault();
}
private void UpdateRecurrenceSummary()
{
if (!HasInitializedComposeDateRange())
{
RecurrenceSummary = string.Empty;
return;
}
var effectiveStart = GetEffectiveStartDateTime();
var effectiveEnd = GetEffectiveEndDateTime();
var selectedDays = IsDailyRecurrenceSelected
? WeekdayOptions
.Where(option => option.IsSelected)
.Select(option => option.DayOfWeek)
.ToList()
: [];
RecurrenceSummary = CalendarRecurrenceSummaryFormatter.BuildSummary(
IsRecurring,
effectiveStart,
effectiveEnd,
IsAllDay,
CurrentSettings,
SelectedRecurrenceInterval,
SelectedRecurrenceFrequencyOption?.Frequency ?? CalendarItemRecurrenceFrequency.Weekly,
selectedDays,
RecurrenceEndDate);
}
private bool HasInitializedComposeDateRange()
{
if (StartDate == default)
{
return false;
}
return !IsAllDay || AllDayEndDate != default;
}
private string BuildRecurrenceRule()
{
if (!IsRecurring || SelectedRecurrenceFrequencyOption == null)
return string.Empty;
var parts = new List<string>
{
$"FREQ={SelectedRecurrenceFrequencyOption.Frequency.ToString().ToUpperInvariant()}",
$"INTERVAL={SelectedRecurrenceInterval}"
};
var selectedDays = IsDailyRecurrenceSelected
? WeekdayOptions
.Where(option => option.IsSelected)
.Select(option => option.RuleValue)
.ToList()
: [];
if (selectedDays.Count > 0)
{
parts.Add($"BYDAY={string.Join(",", selectedDays)}");
}
if (RecurrenceEndDate.HasValue)
{
var untilValue = IsAllDay
? RecurrenceEndDate.Value.ToString("yyyyMMdd", CultureInfo.InvariantCulture)
: RecurrenceEndDate.Value.Date.AddDays(1).AddSeconds(-1).ToString("yyyyMMdd'T'HHmmss", CultureInfo.InvariantCulture);
parts.Add($"UNTIL={untilValue}");
}
return $"RRULE:{string.Join(";", parts)}";
}
private DateTime GetEffectiveStartDateTime()
=> StartDate.Date.Add(IsAllDay ? TimeSpan.Zero : StartTime);
private DateTime GetEffectiveEndDateTime()
=> IsAllDay
? AllDayEndDate.Date
: StartDate.Date.Add(EndTime);
private static (DateTime StartDate, DateTime EndDate) GetDefaultComposeDateRange()
{
var localNow = DateTime.Now;
var roundedMinutes = localNow.Minute switch
{
< 30 => 30,
30 when localNow.Second == 0 && localNow.Millisecond == 0 => 30,
_ => 60
};
var startDate = new DateTime(localNow.Year, localNow.Month, localNow.Day, localNow.Hour, 0, 0);
startDate = roundedMinutes == 60 ? startDate.AddHours(1) : startDate.AddMinutes(roundedMinutes);
return (startDate, startDate.AddMinutes(30));
}
private CalendarComposeWeekdayOption CreateWeekdayOption(DayOfWeek dayOfWeek, string ruleValue, string label)
{
var option = new CalendarComposeWeekdayOption(dayOfWeek, ruleValue, label);
option.PropertyChanged += WeekdayOptionPropertyChanged;
return option;
}
private void WeekdayOptionPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(CalendarComposeWeekdayOption.IsSelected))
{
UpdateRecurrenceSummary();
}
}
private void SelectSingleWeekday(DayOfWeek dayOfWeek)
{
foreach (var option in WeekdayOptions)
{
option.IsSelected = option.DayOfWeek == dayOfWeek;
}
}
private void ShowValidationMessage(string message)
{
_dialogService.InfoBarMessage(
Translator.CalendarEventCompose_ValidationTitle,
message,
InfoBarMessageType.Warning);
}
private void AttachmentsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged(nameof(HasAttachments));
}
private bool TryAddAttachment(string fileName, string filePath, string fileExtension, long size)
{
if (!CanAddAttachments ||
string.IsNullOrWhiteSpace(filePath) ||
Attachments.Any(existing => existing.FilePath.Equals(filePath, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
Attachments.Add(new CalendarComposeAttachmentViewModel(fileName, filePath, fileExtension, size));
return true;
}
}
public partial class CalendarComposeFrequencyOption : ObservableObject
{
public CalendarItemRecurrenceFrequency Frequency { get; }
public string DisplayText { get; }
public CalendarComposeFrequencyOption(CalendarItemRecurrenceFrequency frequency, string displayText)
{
Frequency = frequency;
DisplayText = displayText;
}
public string PluralLabel(int interval)
{
if (interval == 1)
return DisplayText;
return Frequency switch
{
CalendarItemRecurrenceFrequency.Daily => Translator.CalendarEventCompose_FrequencyDayPlural,
CalendarItemRecurrenceFrequency.Weekly => Translator.CalendarEventCompose_FrequencyWeekPlural,
CalendarItemRecurrenceFrequency.Monthly => Translator.CalendarEventCompose_FrequencyMonthPlural,
CalendarItemRecurrenceFrequency.Yearly => Translator.CalendarEventCompose_FrequencyYearPlural,
_ => DisplayText
};
}
}
public partial class CalendarComposeWeekdayOption : ObservableObject
{
public DayOfWeek DayOfWeek { get; }
public string RuleValue { get; }
public string Label { get; }
public string FullDayName => DayOfWeek switch
{
DayOfWeek.Monday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[1],
DayOfWeek.Tuesday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[2],
DayOfWeek.Wednesday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[3],
DayOfWeek.Thursday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[4],
DayOfWeek.Friday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[5],
DayOfWeek.Saturday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[6],
DayOfWeek.Sunday => CultureInfo.CurrentCulture.DateTimeFormat.DayNames[0],
_ => string.Empty
};
[ObservableProperty]
public partial bool IsSelected { get; set; }
public CalendarComposeWeekdayOption(DayOfWeek dayOfWeek, string ruleValue, string label)
{
DayOfWeek = dayOfWeek;
RuleValue = ruleValue;
Label = label;
}
}
@@ -0,0 +1,44 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.ViewModels;
public partial class CalendarNotificationSettingsPageViewModel : CalendarSettingsSectionViewModelBase
{
[ObservableProperty]
public partial int SelectedDefaultReminderIndex { get; set; }
[ObservableProperty]
public partial int SelectedDefaultSnoozeIndex { get; set; }
public CalendarNotificationSettingsPageViewModel(
IPreferencesService preferencesService,
ICalendarService calendarService,
IAccountService accountService)
: base(preferencesService, calendarService, accountService)
{
LoadReminderOptions();
LoadSnoozeOptions();
SelectedDefaultReminderIndex = GetSelectedReminderIndex();
SelectedDefaultSnoozeIndex = GetSelectedSnoozeIndex();
IsLoaded = true;
}
partial void OnSelectedDefaultReminderIndexChanged(int value)
{
if (!IsLoaded)
return;
SaveReminderIndex(value);
}
partial void OnSelectedDefaultSnoozeIndexChanged(int value)
{
if (!IsLoaded)
return;
SaveSnoozeIndex(value);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,62 @@
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.ViewModels;
public partial class CalendarPreferenceSettingsPageViewModel : CalendarSettingsSectionViewModelBase
{
[ObservableProperty]
public partial CalendarNewEventBehaviorOption SelectedNewEventBehaviorOption { get; set; }
[ObservableProperty]
public partial AccountCalendarViewModel SelectedNewEventCalendar { get; set; }
public bool ShouldShowSpecificNewEventCalendar
=> SelectedNewEventBehaviorOption?.Behavior == NewEventButtonBehavior.AlwaysUseSpecificCalendar;
public CalendarPreferenceSettingsPageViewModel(
IPreferencesService preferencesService,
ICalendarService calendarService,
IAccountService accountService)
: base(preferencesService, calendarService, accountService)
{
LoadNewEventBehaviorOptions();
SelectedNewEventBehaviorOption = GetSelectedNewEventBehaviorOption();
IsLoaded = true;
LoadCalendarsAsync(ApplyStoredNewEventCalendarPreference);
}
partial void OnSelectedNewEventBehaviorOptionChanged(CalendarNewEventBehaviorOption value)
{
if (!IsLoaded)
return;
OnPropertyChanged(nameof(ShouldShowSpecificNewEventCalendar));
SaveNewEventBehavior(SelectedNewEventBehaviorOption, SelectedNewEventCalendar);
}
partial void OnSelectedNewEventCalendarChanged(AccountCalendarViewModel value)
{
if (!IsLoaded)
return;
SaveNewEventBehavior(SelectedNewEventBehaviorOption, value);
}
private void ApplyStoredNewEventCalendarPreference()
{
var configuredCalendar = ResolveSelectedNewEventCalendar();
if (PreferencesService.NewEventButtonBehavior == NewEventButtonBehavior.AlwaysUseSpecificCalendar && configuredCalendar == null)
{
SelectedNewEventBehaviorOption = NewEventBehaviorOptions.First(option => option.Behavior == NewEventButtonBehavior.AskEachTime);
SelectedNewEventCalendar = null;
return;
}
SelectedNewEventCalendar = configuredCalendar ?? ResolveFallbackNewEventCalendar();
}
}
@@ -0,0 +1,191 @@
using System;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.ViewModels;
public partial class CalendarRenderingSettingsPageViewModel : CalendarSettingsSectionViewModelBase
{
[ObservableProperty]
public partial double CellHourHeight { get; set; }
[ObservableProperty]
public partial int SelectedFirstDayOfWeekIndex { get; set; }
[ObservableProperty]
public partial bool Is24HourHeaders { get; set; }
[ObservableProperty]
public partial bool IsWorkingHoursEnabled { get; set; }
[ObservableProperty]
public partial TimeSpan WorkingHourStart { get; set; }
[ObservableProperty]
public partial TimeSpan WorkingHourEnd { get; set; }
[ObservableProperty]
public partial int WorkingDayStartIndex { get; set; }
[ObservableProperty]
public partial int WorkingDayEndIndex { get; set; }
[ObservableProperty]
public partial string TimedDayHeaderDateFormat { get; set; } = "ddd dd";
[ObservableProperty]
public partial int SelectedTimedDayHeaderFormatPresetIndex { get; set; } = -1;
public CalendarRenderingSettingsPageViewModel(
IPreferencesService preferencesService,
ICalendarService calendarService,
IAccountService accountService)
: base(preferencesService, calendarService, accountService)
{
SelectedFirstDayOfWeekIndex = DayNames.IndexOf(CalendarCulture.DateTimeFormat.GetDayName(preferencesService.FirstDayOfWeek));
Is24HourHeaders = preferencesService.Prefer24HourTimeFormat;
IsWorkingHoursEnabled = preferencesService.IsWorkingHoursEnabled;
WorkingHourStart = preferencesService.WorkingHourStart;
WorkingHourEnd = preferencesService.WorkingHourEnd;
CellHourHeight = preferencesService.HourHeight;
WorkingDayStartIndex = DayNames.IndexOf(CalendarCulture.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart));
WorkingDayEndIndex = DayNames.IndexOf(CalendarCulture.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd));
TimedDayHeaderDateFormat = preferencesService.CalendarTimedDayHeaderDateFormat;
SelectedTimedDayHeaderFormatPresetIndex = TimedDayHeaderFormatPresets.IndexOf(TimedDayHeaderDateFormat);
IsLoaded = true;
}
partial void OnCellHourHeightChanged(double oldValue, double newValue) => SaveSettings();
partial void OnIs24HourHeadersChanged(bool value)
{
OnPropertyChanged(nameof(TimedHourLabelPreview));
SaveSettings();
}
partial void OnSelectedFirstDayOfWeekIndexChanged(int value) => SaveSettings();
partial void OnIsWorkingHoursEnabledChanged(bool value) => SaveSettings();
partial void OnWorkingHourStartChanged(TimeSpan value) => SaveSettings();
partial void OnWorkingHourEndChanged(TimeSpan value) => SaveSettings();
partial void OnWorkingDayStartIndexChanged(int value) => SaveSettings();
partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings();
partial void OnTimedDayHeaderDateFormatChanged(string value)
{
OnPropertyChanged(nameof(TimedDayHeaderFormatPreview));
OnPropertyChanged(nameof(TimedHourLabelPreview));
var normalizedFormat = string.IsNullOrWhiteSpace(value) ? "ddd dd" : value.Trim();
var matchingPresetIndex = TimedDayHeaderFormatPresets
.Select((format, index) => new { format, index })
.Where(item => string.Equals(item.format, normalizedFormat, StringComparison.Ordinal))
.Select(item => item.index)
.DefaultIfEmpty(-1)
.First();
if (SelectedTimedDayHeaderFormatPresetIndex != matchingPresetIndex)
{
SelectedTimedDayHeaderFormatPresetIndex = matchingPresetIndex;
}
SaveSettings();
}
partial void OnSelectedTimedDayHeaderFormatPresetIndexChanged(int value)
{
if (value < 0 || value >= TimedDayHeaderFormatPresets.Count)
return;
var selectedPreset = TimedDayHeaderFormatPresets[value];
if (string.Equals(TimedDayHeaderDateFormat, selectedPreset, StringComparison.Ordinal))
return;
TimedDayHeaderDateFormat = selectedPreset;
}
public string TimedDayHeaderFormatPreview
{
get
{
var format = string.IsNullOrWhiteSpace(TimedDayHeaderDateFormat) ? "ddd dd" : TimedDayHeaderDateFormat.Trim();
var previewDates = new[]
{
new DateTime(2026, 3, 23),
new DateTime(2026, 3, 24),
new DateTime(2026, 3, 25)
};
try
{
return string.Join(" · ", previewDates.Select(date => date.ToString(format, CalendarCulture)));
}
catch (FormatException)
{
return string.Join(" · ", previewDates.Select(date => date.ToString("ddd dd", CalendarCulture)));
}
}
}
public string TimedHourLabelPreview
=> string.Join(" · ", new[] { 0, 9, 14, 24 }.Select(CurrentSettingsPreviewLabel));
private string CurrentSettingsPreviewLabel(int hour)
{
if (Is24HourHeaders)
return hour.ToString(CalendarCulture);
var displayHour = hour % 24;
return DateTime.Today.AddHours(displayHour).ToString("h tt", CalendarCulture);
}
private void SaveSettings()
{
if (!IsLoaded)
return;
PreferencesService.FirstDayOfWeek = SelectedFirstDayOfWeekIndex switch
{
0 => DayOfWeek.Sunday,
1 => DayOfWeek.Monday,
2 => DayOfWeek.Tuesday,
3 => DayOfWeek.Wednesday,
4 => DayOfWeek.Thursday,
5 => DayOfWeek.Friday,
6 => DayOfWeek.Saturday,
_ => throw new ArgumentOutOfRangeException()
};
PreferencesService.WorkingDayStart = WorkingDayStartIndex switch
{
0 => DayOfWeek.Sunday,
1 => DayOfWeek.Monday,
2 => DayOfWeek.Tuesday,
3 => DayOfWeek.Wednesday,
4 => DayOfWeek.Thursday,
5 => DayOfWeek.Friday,
6 => DayOfWeek.Saturday,
_ => throw new ArgumentOutOfRangeException()
};
PreferencesService.WorkingDayEnd = WorkingDayEndIndex switch
{
0 => DayOfWeek.Sunday,
1 => DayOfWeek.Monday,
2 => DayOfWeek.Tuesday,
3 => DayOfWeek.Wednesday,
4 => DayOfWeek.Thursday,
5 => DayOfWeek.Friday,
6 => DayOfWeek.Saturday,
_ => throw new ArgumentOutOfRangeException()
};
PreferencesService.Prefer24HourTimeFormat = Is24HourHeaders;
PreferencesService.IsWorkingHoursEnabled = IsWorkingHoursEnabled;
PreferencesService.WorkingHourStart = WorkingHourStart;
PreferencesService.WorkingHourEnd = WorkingHourEnd;
PreferencesService.HourHeight = CellHourHeight;
PreferencesService.CalendarTimedDayHeaderDateFormat = TimedDayHeaderDateFormat;
}
}
@@ -0,0 +1,197 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Translations;
using Wino.Core.ViewModels;
namespace Wino.Calendar.ViewModels;
public abstract class CalendarSettingsSectionViewModelBase : CalendarBaseViewModel
{
protected CalendarSettingsSectionViewModelBase(
IPreferencesService preferencesService,
ICalendarService calendarService,
IAccountService accountService)
{
PreferencesService = preferencesService;
CalendarService = calendarService;
AccountService = accountService;
var languageCode = WinoTranslationDictionary.GetLanguageFileNameRelativePath(preferencesService.CurrentLanguage);
CalendarCulture = new CultureInfo(languageCode);
for (var index = 0; index < 7; index++)
{
DayNames.Add(CalendarCulture.DateTimeFormat.DayNames[index]);
}
}
protected IPreferencesService PreferencesService { get; }
protected ICalendarService CalendarService { get; }
protected IAccountService AccountService { get; }
protected CultureInfo CalendarCulture { get; }
protected bool IsLoaded { get; set; }
public ObservableCollection<string> DayNames { get; } = [];
public ObservableCollection<string> ReminderOptions { get; } = [];
public ObservableCollection<string> SnoozeOptions { get; } = [];
public ObservableCollection<CalendarNewEventBehaviorOption> NewEventBehaviorOptions { get; } = [];
public ObservableCollection<AccountCalendarViewModel> AvailableNewEventCalendars { get; } = [];
public ObservableCollection<string> TimedDayHeaderFormatPresets { get; } =
[
"ddd dd",
"dddd dd",
"ddd d MMM",
"dd MMM ddd",
"M/d ddd"
];
protected void LoadReminderOptions()
{
ReminderOptions.Clear();
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);
}
}
protected int GetSelectedReminderIndex()
{
if (PreferencesService.DefaultReminderDurationInSeconds == 0)
return 0;
var minutes = (int)(PreferencesService.DefaultReminderDurationInSeconds / 60);
var predefinedMinutes = CalendarService.GetPredefinedReminderMinutes();
var index = Array.IndexOf(predefinedMinutes, minutes);
return index >= 0 ? index + 1 : 0;
}
protected void SaveReminderIndex(int selectedDefaultReminderIndex)
{
if (selectedDefaultReminderIndex == 0)
{
PreferencesService.DefaultReminderDurationInSeconds = 0;
return;
}
var predefinedMinutes = CalendarService.GetPredefinedReminderMinutes();
var minutes = predefinedMinutes[selectedDefaultReminderIndex - 1];
PreferencesService.DefaultReminderDurationInSeconds = minutes * 60;
}
protected void LoadSnoozeOptions()
{
SnoozeOptions.Clear();
foreach (var snoozeMinutes in CalendarReminderSnoozeOptions.GetSupportedSnoozeMinutes())
{
SnoozeOptions.Add(string.Format(Translator.CalendarReminder_SnoozeMinutesOption, snoozeMinutes));
}
}
protected int GetSelectedSnoozeIndex()
{
var supportedSnoozeMinutes = CalendarReminderSnoozeOptions.GetSupportedSnoozeMinutes().ToArray();
var selectedIndex = Array.IndexOf(supportedSnoozeMinutes, PreferencesService.DefaultSnoozeDurationInMinutes);
return selectedIndex >= 0 ? selectedIndex : 0;
}
protected void SaveSnoozeIndex(int selectedDefaultSnoozeIndex)
{
var supportedSnoozeMinutes = CalendarReminderSnoozeOptions.GetSupportedSnoozeMinutes();
if (supportedSnoozeMinutes.Count == 0)
return;
var selectedIndex = Math.Clamp(selectedDefaultSnoozeIndex, 0, supportedSnoozeMinutes.Count - 1);
PreferencesService.DefaultSnoozeDurationInMinutes = supportedSnoozeMinutes[selectedIndex];
}
protected void LoadNewEventBehaviorOptions()
{
NewEventBehaviorOptions.Clear();
NewEventBehaviorOptions.Add(new CalendarNewEventBehaviorOption(NewEventButtonBehavior.AskEachTime, Translator.CalendarSettings_NewEventBehavior_AskEachTime));
NewEventBehaviorOptions.Add(new CalendarNewEventBehaviorOption(NewEventButtonBehavior.AlwaysUseSpecificCalendar, Translator.CalendarSettings_NewEventBehavior_AlwaysUseSpecificCalendar));
}
protected CalendarNewEventBehaviorOption GetSelectedNewEventBehaviorOption()
=> NewEventBehaviorOptions.FirstOrDefault(option => option.Behavior == PreferencesService.NewEventButtonBehavior)
?? NewEventBehaviorOptions.First();
protected async void LoadCalendarsAsync(Action applySelection)
{
var accounts = await AccountService.GetAccountsAsync().ConfigureAwait(false);
var calendarsByAccount = new List<AccountCalendarViewModel>();
foreach (var account in accounts)
{
var calendars = await CalendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
calendarsByAccount.AddRange(calendars.Select(calendar => new AccountCalendarViewModel(account, calendar)));
}
await ExecuteUIThread(() =>
{
AvailableNewEventCalendars.Clear();
foreach (var calendar in calendarsByAccount)
{
AvailableNewEventCalendars.Add(calendar);
}
applySelection();
});
}
protected AccountCalendarViewModel ResolveSelectedNewEventCalendar()
{
var configuredCalendarId = PreferencesService.DefaultNewEventCalendarId;
return configuredCalendarId.HasValue
? AvailableNewEventCalendars.FirstOrDefault(calendar => calendar.Id == configuredCalendarId.Value)
: null;
}
protected AccountCalendarViewModel ResolveFallbackNewEventCalendar()
=> AvailableNewEventCalendars.FirstOrDefault(calendar => calendar.IsPrimary)
?? AvailableNewEventCalendars.FirstOrDefault();
protected void SaveNewEventBehavior(CalendarNewEventBehaviorOption selectedBehaviorOption, AccountCalendarViewModel selectedCalendar)
{
var newEventBehavior = selectedBehaviorOption?.Behavior ?? NewEventButtonBehavior.AskEachTime;
if (newEventBehavior == NewEventButtonBehavior.AlwaysUseSpecificCalendar && selectedCalendar != null)
{
PreferencesService.NewEventButtonBehavior = NewEventButtonBehavior.AlwaysUseSpecificCalendar;
PreferencesService.DefaultNewEventCalendarId = selectedCalendar.Id;
return;
}
PreferencesService.NewEventButtonBehavior = NewEventButtonBehavior.AskEachTime;
PreferencesService.DefaultNewEventCalendarId = null;
}
}
public sealed class CalendarNewEventBehaviorOption
{
public CalendarNewEventBehaviorOption(NewEventButtonBehavior behavior, string displayText)
{
Behavior = behavior;
DisplayText = displayText;
}
public NewEventButtonBehavior Behavior { get; }
public string DisplayText { get; }
}
@@ -0,0 +1,93 @@
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.ViewModels.Data;
public partial class AccountCalendarViewModel : ObservableObject, IAccountCalendar
{
public MailAccount Account { get; }
public AccountCalendar AccountCalendar { get; }
public AccountCalendarViewModel(MailAccount account, AccountCalendar accountCalendar)
{
Account = account;
AccountCalendar = accountCalendar;
IsChecked = accountCalendar.IsExtended;
}
[ObservableProperty]
private bool _isChecked;
partial void OnIsCheckedChanged(bool value) => IsExtended = value;
public string Name
{
get => AccountCalendar.Name;
set => SetProperty(AccountCalendar.Name, value, AccountCalendar, (u, n) => u.Name = n);
}
public string TextColorHex
{
get => AccountCalendar.TextColorHex;
set => SetProperty(AccountCalendar.TextColorHex, value, AccountCalendar, (u, t) => u.TextColorHex = t);
}
public string BackgroundColorHex
{
get => AccountCalendar.BackgroundColorHex;
set => SetProperty(AccountCalendar.BackgroundColorHex, value, AccountCalendar, (u, b) => u.BackgroundColorHex = b);
}
public bool IsExtended
{
get => AccountCalendar.IsExtended;
set => SetProperty(AccountCalendar.IsExtended, value, AccountCalendar, (u, i) => u.IsExtended = i);
}
public bool IsPrimary
{
get => AccountCalendar.IsPrimary;
set => SetProperty(AccountCalendar.IsPrimary, value, AccountCalendar, (u, i) => u.IsPrimary = i);
}
public bool IsReadOnly
{
get => AccountCalendar.IsReadOnly;
set => SetProperty(AccountCalendar.IsReadOnly, value, AccountCalendar, (u, i) => u.IsReadOnly = i);
}
public bool IsSynchronizationEnabled
{
get => AccountCalendar.IsSynchronizationEnabled;
set => SetProperty(AccountCalendar.IsSynchronizationEnabled, value, AccountCalendar, (u, i) => u.IsSynchronizationEnabled = i);
}
public Guid AccountId
{
get => AccountCalendar.AccountId;
set => SetProperty(AccountCalendar.AccountId, value, AccountCalendar, (u, a) => u.AccountId = a);
}
public string RemoteCalendarId
{
get => AccountCalendar.RemoteCalendarId;
set => SetProperty(AccountCalendar.RemoteCalendarId, value, AccountCalendar, (u, r) => u.RemoteCalendarId = r);
}
public CalendarItemShowAs DefaultShowAs
{
get => AccountCalendar.DefaultShowAs;
set => SetProperty(AccountCalendar.DefaultShowAs, value, AccountCalendar, (u, s) => u.DefaultShowAs = s);
}
public Guid Id { get => ((IAccountCalendar)AccountCalendar).Id; set => ((IAccountCalendar)AccountCalendar).Id = value; }
public MailAccount MailAccount
{
get => AccountCalendar.MailAccount ?? Account;
set => AccountCalendar.MailAccount = value;
}
}
@@ -0,0 +1,71 @@
using System;
using System.IO;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Enums;
using Wino.Core.Extensions;
namespace Wino.Calendar.ViewModels.Data;
public partial class CalendarAttachmentViewModel : ObservableObject
{
public CalendarAttachment Attachment { get; }
public Guid Id => Attachment.Id;
public string FileName => Attachment.FileName;
public string ReadableSize { get; }
public MailAttachmentType AttachmentType { get; }
public bool IsDownloaded => Attachment.IsDownloaded;
[ObservableProperty]
public partial bool IsBusy { get; set; }
public CalendarAttachmentViewModel(CalendarAttachment attachment)
{
Attachment = attachment;
ReadableSize = attachment.Size.GetBytesReadable();
var extension = Path.GetExtension(FileName);
AttachmentType = GetAttachmentType(extension);
}
private MailAttachmentType GetAttachmentType(string extension)
{
if (string.IsNullOrEmpty(extension))
return MailAttachmentType.None;
switch (extension.ToLower())
{
case ".exe":
return MailAttachmentType.Executable;
case ".rar":
return MailAttachmentType.RarArchive;
case ".zip":
return MailAttachmentType.Archive;
case ".ogg":
case ".mp3":
case ".wav":
case ".aac":
case ".alac":
return MailAttachmentType.Audio;
case ".mp4":
case ".wmv":
case ".avi":
case ".flv":
return MailAttachmentType.Video;
case ".pdf":
return MailAttachmentType.PDF;
case ".htm":
case ".html":
return MailAttachmentType.HTML;
case ".png":
case ".jpg":
case ".jpeg":
case ".gif":
case ".jiff":
return MailAttachmentType.Image;
default:
return MailAttachmentType.Other;
}
}
}
@@ -0,0 +1,57 @@
using System;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Extensions;
namespace Wino.Calendar.ViewModels.Data;
public class CalendarComposeAttachmentViewModel
{
public Guid Id { get; } = Guid.NewGuid();
public string FileName { get; }
public string FilePath { get; }
public string FileExtension { get; }
public long Size { get; }
public string ReadableSize => Size.GetBytesReadable();
public MailAttachmentType AttachmentType { get; }
public CalendarComposeAttachmentViewModel(string fileName, string filePath, string fileExtension, long size)
{
FileName = fileName;
FilePath = filePath;
FileExtension = fileExtension;
Size = size;
AttachmentType = GetAttachmentType(fileExtension);
}
public CalendarEventComposeAttachmentDraft ToDraftModel()
{
return new CalendarEventComposeAttachmentDraft
{
Id = Id,
FileName = FileName,
FilePath = FilePath,
FileExtension = FileExtension,
Size = Size
};
}
private static MailAttachmentType GetAttachmentType(string extension)
{
if (string.IsNullOrWhiteSpace(extension))
return MailAttachmentType.None;
return extension.ToLowerInvariant() switch
{
".exe" => MailAttachmentType.Executable,
".rar" => MailAttachmentType.RarArchive,
".zip" => MailAttachmentType.Archive,
".ogg" or ".mp3" or ".wav" or ".aac" or ".alac" => MailAttachmentType.Audio,
".mp4" or ".wmv" or ".avi" or ".flv" => MailAttachmentType.Video,
".pdf" => MailAttachmentType.PDF,
".htm" or ".html" => MailAttachmentType.HTML,
".png" or ".jpg" or ".jpeg" or ".gif" or ".jiff" => MailAttachmentType.Image,
_ => MailAttachmentType.Other
};
}
}
@@ -0,0 +1,23 @@
using Wino.Core.Domain.Entities.Shared;
namespace Wino.Calendar.ViewModels.Data;
public class CalendarComposeAttendeeViewModel : IContactDisplayItem
{
public string DisplayName { get; }
public string Email { get; }
public AccountContact ResolvedContact { get; }
public string Address => Email;
public AccountContact PreviewContact => ResolvedContact;
public bool HasDistinctDisplayName => !string.IsNullOrWhiteSpace(DisplayName) && !DisplayName.Equals(Email, System.StringComparison.OrdinalIgnoreCase);
public CalendarComposeAttendeeViewModel(string displayName, string email, AccountContact resolvedContact = null)
{
DisplayName = string.IsNullOrWhiteSpace(displayName) ? email : displayName;
Email = email;
ResolvedContact = resolvedContact;
}
public static CalendarComposeAttendeeViewModel FromContact(AccountContact contact)
=> new(contact.Name, contact.Address, contact);
}
@@ -0,0 +1,198 @@
using System;
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Itenso.TimePeriod;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.ViewModels.Data;
public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, ICalendarItemViewModel
{
public CalendarItem CalendarItem { get; }
public string Title => CalendarItem.Title;
public Guid Id => CalendarItem.Id;
public IAccountCalendar AssignedCalendar => CalendarItem.AssignedCalendar;
/// <summary>
/// Gets or sets the start date converted to user's local timezone for display.
/// The underlying CalendarItem stores dates according to their timezone.
/// </summary>
public DateTime StartDate
{
get
{
// Get start date in user's local timezone
return CalendarItem.LocalStartDate;
}
set
{
// All-day events use floating dates and should not shift across timezones.
CalendarItem.StartDate = CalendarItem.IsAllDayEvent
? value.Date
: value.ToTimeZoneFromLocal(CalendarItem.StartTimeZone);
}
}
/// <summary>
/// Gets the end date converted to user's local timezone for display.
/// The underlying CalendarItem stores dates according to their timezone.
/// </summary>
public DateTime EndDate
{
get
{
// Get end date in user's local timezone
return CalendarItem.LocalEndDate;
}
}
public double DurationInSeconds { get => CalendarItem.DurationInSeconds; set => CalendarItem.DurationInSeconds = value; }
/// <summary>
/// Gets the time period in local time.
/// </summary>
public ITimePeriod Period
{
get
{
// Return a period using local times for UI display
return new TimeRange(StartDate, EndDate);
}
}
public bool IsAllDayEvent => CalendarItem.IsAllDayEvent;
public bool IsMultiDayEvent => CalendarItem.IsMultiDayEvent;
public bool IsRecurringEvent => CalendarItem.IsRecurringEvent;
public bool IsRecurringChild => CalendarItem.IsRecurringChild;
public bool IsRecurringParent => CalendarItem.IsRecurringParent;
public bool CanDragDrop => CalendarItem.CanChangeStartAndEndDate;
[ObservableProperty]
public partial bool IsSelected { get; set; }
[ObservableProperty]
public partial bool IsBusy { get; set; }
/// <summary>
/// The period of the day where this item is currently being displayed.
/// Used for multi-day event title formatting.
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(DisplayTitle))]
public partial ITimePeriod DisplayingPeriod { get; set; }
/// <summary>
/// Calendar settings for time formatting.
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(DisplayTitle))]
public partial CalendarSettings CalendarSettings { get; set; }
/// <summary>
/// Gets the display title based on the current displaying period.
/// </summary>
public string DisplayTitle
{
get
{
if (DisplayingPeriod == null || CalendarSettings == null)
return Title;
return GetDisplayTitle(DisplayingPeriod, CalendarSettings);
}
}
public ObservableCollection<CalendarEventAttendee> Attendees { get; } = new ObservableCollection<CalendarEventAttendee>();
public CalendarItemViewModel(CalendarItem calendarItem)
{
CalendarItem = calendarItem;
}
/// <summary>
/// Updates the underlying CalendarItem with new data and raises property change notifications.
/// </summary>
/// <param name="calendarItem">The updated calendar item data.</param>
public void UpdateFrom(CalendarItem calendarItem)
{
if (calendarItem == null || calendarItem.Id != CalendarItem.Id)
return;
// Update all mutable properties
CalendarItem.Title = calendarItem.Title;
CalendarItem.Description = calendarItem.Description;
CalendarItem.Location = calendarItem.Location;
CalendarItem.StartDate = calendarItem.StartDate;
CalendarItem.StartTimeZone = calendarItem.StartTimeZone;
CalendarItem.EndTimeZone = calendarItem.EndTimeZone;
CalendarItem.DurationInSeconds = calendarItem.DurationInSeconds;
CalendarItem.Recurrence = calendarItem.Recurrence;
CalendarItem.RecurringCalendarItemId = calendarItem.RecurringCalendarItemId;
CalendarItem.OrganizerDisplayName = calendarItem.OrganizerDisplayName;
CalendarItem.OrganizerEmail = calendarItem.OrganizerEmail;
CalendarItem.IsLocked = calendarItem.IsLocked;
CalendarItem.IsHidden = calendarItem.IsHidden;
CalendarItem.CustomEventColorHex = calendarItem.CustomEventColorHex;
CalendarItem.HtmlLink = calendarItem.HtmlLink;
CalendarItem.Status = calendarItem.Status;
CalendarItem.Visibility = calendarItem.Visibility;
CalendarItem.ShowAs = calendarItem.ShowAs;
CalendarItem.UpdatedAt = calendarItem.UpdatedAt;
CalendarItem.AssignedCalendar = calendarItem.AssignedCalendar;
// Raise property changed for all bindable properties
OnPropertyChanged(nameof(Title));
OnPropertyChanged(nameof(StartDate));
OnPropertyChanged(nameof(EndDate));
OnPropertyChanged(nameof(DurationInSeconds));
OnPropertyChanged(nameof(Period));
OnPropertyChanged(nameof(IsAllDayEvent));
OnPropertyChanged(nameof(IsMultiDayEvent));
OnPropertyChanged(nameof(IsRecurringEvent));
OnPropertyChanged(nameof(IsRecurringChild));
OnPropertyChanged(nameof(IsRecurringParent));
OnPropertyChanged(nameof(CanDragDrop));
OnPropertyChanged(nameof(AssignedCalendar));
OnPropertyChanged(nameof(DisplayTitle));
}
/// <summary>
/// Gets the display title for this calendar item when rendered in a specific day.
/// </summary>
public string GetDisplayTitle(ITimePeriod displayingPeriod, CalendarSettings calendarSettings)
{
if (!IsMultiDayEvent)
return Title;
var periodRelation = Period.GetRelation(displayingPeriod);
if (periodRelation == PeriodRelation.StartInside || periodRelation == PeriodRelation.EnclosingStartTouching)
{
// Event starts within this day: "HH:mm -> Title"
return $"{calendarSettings.GetTimeString(StartDate.TimeOfDay)} -> {Title}";
}
else if (periodRelation == PeriodRelation.EndInside || periodRelation == PeriodRelation.EnclosingEndTouching)
{
// Event ends within this day: "Title <- HH:mm"
return $"{Title} <- {calendarSettings.GetTimeString(EndDate.TimeOfDay)}";
}
else if (periodRelation == PeriodRelation.Enclosing)
{
// Event spans the entire day
return $"{Translator.CalendarItemAllDay} {Title}";
}
else
{
return Title;
}
}
public override string ToString() => CalendarItem.Title;
}
@@ -0,0 +1,207 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Models.Synchronization;
namespace Wino.Calendar.ViewModels.Data;
public partial class GroupedAccountCalendarViewModel : ObservableObject
{
public event EventHandler CollectiveSelectionStateChanged;
public event EventHandler<AccountCalendarViewModel> CalendarSelectionStateChanged;
public MailAccount Account { get; }
public ObservableCollection<AccountCalendarViewModel> AccountCalendars { get; }
public static bool SupportsCalendar(MailAccount account)
=> account?.IsCalendarAccessGranted == true;
public GroupedAccountCalendarViewModel(MailAccount account, IEnumerable<AccountCalendarViewModel> calendarViewModels)
{
Account = account;
AccountCalendars = new ObservableCollection<AccountCalendarViewModel>(calendarViewModels);
AccountColorHex = account.AccountColorHex;
ManageIsCheckedState();
foreach (var calendarViewModel in calendarViewModels)
{
calendarViewModel.PropertyChanged += CalendarPropertyChanged;
}
AccountCalendars.CollectionChanged += CalendarListUpdated;
}
private void CalendarListUpdated(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (AccountCalendarViewModel calendar in e.NewItems)
{
calendar.PropertyChanged += CalendarPropertyChanged;
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (AccountCalendarViewModel calendar in e.OldItems)
{
calendar.PropertyChanged -= CalendarPropertyChanged;
}
}
else if (e.Action == NotifyCollectionChangedAction.Reset)
{
foreach (AccountCalendarViewModel calendar in e.OldItems)
{
calendar.PropertyChanged -= CalendarPropertyChanged;
}
}
}
private void CalendarPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (sender is AccountCalendarViewModel viewModel &&
e.PropertyName == nameof(AccountCalendarViewModel.IsChecked))
{
ManageIsCheckedState();
UpdateCalendarCheckedState(viewModel, viewModel.IsChecked, true);
}
}
[ObservableProperty]
public partial bool IsExpanded { get; set; } = true;
[ObservableProperty]
public partial bool? IsCheckedState { get; set; } = true;
[ObservableProperty]
public partial string AccountColorHex { get; set; } = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanSynchronize), nameof(IsSynchronizationProgressVisible), nameof(IsProgressIndeterminate))]
public partial bool IsSynchronizationInProgress { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(SynchronizationProgressValue), nameof(IsProgressIndeterminate))]
public partial int TotalItemsToSync { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(SynchronizationProgressValue), nameof(IsProgressIndeterminate))]
public partial int RemainingItemsToSync { get; set; }
[ObservableProperty]
public partial string SynchronizationStatus { get; set; } = string.Empty;
public bool CanSynchronize => !IsSynchronizationInProgress;
public bool IsSynchronizationProgressVisible => IsSynchronizationInProgress;
public bool IsProgressIndeterminate => IsSynchronizationInProgress && TotalItemsToSync <= 0;
public string AccountAddressDisplay => string.IsNullOrWhiteSpace(Account?.Address) ? string.Empty : $" ({Account.Address})";
public double SynchronizationProgress
{
get
{
if (TotalItemsToSync <= 0)
return 0;
return ((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100;
}
}
public double SynchronizationProgressValue => SynchronizationProgress;
private bool _isExternalPropChangeBlocked;
public void ApplySynchronizationProgress(AccountSynchronizationProgress progress)
{
if (progress == null || progress.AccountId != Account.Id)
return;
IsSynchronizationInProgress = progress.IsInProgress;
TotalItemsToSync = progress.TotalUnits;
RemainingItemsToSync = progress.RemainingUnits;
SynchronizationStatus = progress.Status ?? string.Empty;
}
private void ManageIsCheckedState()
{
if (_isExternalPropChangeBlocked)
return;
_isExternalPropChangeBlocked = true;
if (AccountCalendars.All(c => c.IsChecked))
{
IsCheckedState = true;
}
else if (AccountCalendars.All(c => !c.IsChecked))
{
IsCheckedState = false;
}
else
{
IsCheckedState = null;
}
_isExternalPropChangeBlocked = false;
}
partial void OnIsCheckedStateChanged(bool? oldValue, bool? newValue)
{
if (_isExternalPropChangeBlocked)
return;
_isExternalPropChangeBlocked = true;
if (newValue == null)
{
foreach (var calendar in AccountCalendars)
{
UpdateCalendarCheckedState(calendar, calendar.IsPrimary);
}
}
else
{
foreach (var calendar in AccountCalendars)
{
UpdateCalendarCheckedState(calendar, newValue.GetValueOrDefault());
}
}
_isExternalPropChangeBlocked = false;
CollectiveSelectionStateChanged?.Invoke(this, EventArgs.Empty);
}
private void UpdateCalendarCheckedState(AccountCalendarViewModel accountCalendarViewModel, bool newValue, bool ignoreValueCheck = false)
{
var currentValue = accountCalendarViewModel.IsChecked;
if (currentValue == newValue && !ignoreValueCheck)
return;
accountCalendarViewModel.IsChecked = newValue;
if (_isExternalPropChangeBlocked)
return;
CalendarSelectionStateChanged?.Invoke(this, accountCalendarViewModel);
}
public void UpdateAccount(MailAccount updatedAccount)
{
if (updatedAccount == null || updatedAccount.Id != Account.Id)
return;
Account.Name = updatedAccount.Name;
Account.Address = updatedAccount.Address;
Account.AccountColorHex = updatedAccount.AccountColorHex;
Account.AttentionReason = updatedAccount.AttentionReason;
Account.MergedInboxId = updatedAccount.MergedInboxId;
AccountColorHex = updatedAccount.AccountColorHex;
OnPropertyChanged(nameof(Account));
OnPropertyChanged(nameof(AccountAddressDisplay));
}
}
@@ -0,0 +1,884 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Serilog;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Services;
using Wino.Core.ViewModels;
using Wino.Messaging.Client.Calendar;
namespace Wino.Calendar.ViewModels;
public partial class EventDetailsPageViewModel : CalendarBaseViewModel
{
private readonly ICalendarService _calendarService;
private readonly INativeAppService _nativeAppService;
private readonly IPreferencesService _preferencesService;
private readonly IMailDialogService _dialogService;
private readonly IWinoRequestDelegator _winoRequestDelegator;
private readonly INavigationService _navigationService;
private readonly IUnderlyingThemeService _underlyingThemeService;
private readonly INotificationBuilder _notificationBuilder;
private readonly IContactService _contactService;
public CalendarSettings CurrentSettings { get; }
public INativeAppService NativeAppService => _nativeAppService;
[ObservableProperty]
public partial bool IsDarkWebviewRenderer { get; set; }
public ObservableCollection<CalendarAttachmentViewModel> Attachments { get; } = new ObservableCollection<CalendarAttachmentViewModel>();
/// <summary>
/// Returns true if the current event has attachments.
/// </summary>
public bool HasAttachments => Attachments.Count > 0;
#region Details
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanViewSeries))]
[NotifyPropertyChangedFor(nameof(CanEditSeries))]
[NotifyPropertyChangedFor(nameof(IsCurrentUserOrganizer))]
[NotifyPropertyChangedFor(nameof(CurrentRsvpText))]
[NotifyPropertyChangedFor(nameof(CurrentRsvpStatus))]
public partial CalendarItemViewModel CurrentEvent { get; set; }
partial void OnCurrentEventChanged(CalendarItemViewModel value)
{
// Notify the view to re-render the description
Messenger.Send(new CalendarDescriptionRenderingRequested());
}
[ObservableProperty]
public partial CalendarItemViewModel SeriesParent { get; set; }
[ObservableProperty]
public partial List<Reminder> Reminders { get; set; }
public ObservableCollection<ReminderOption> ReminderOptions { get; } = new ObservableCollection<ReminderOption>();
/// <summary>
/// Returns true if the event is part of a recurring series (as a child occurrence).
/// Used to enable "View Series" functionality.
/// </summary>
public bool CanViewSeries => CurrentEvent?.IsRecurringChild ?? false;
/// <summary>
/// Returns true if the "Edit Series" button should be visible.
/// Only visible for child occurrences of recurring events, not for master events or single events.
/// </summary>
public bool CanEditSeries => CurrentEvent?.IsRecurringChild ?? false;
/// <summary>
/// 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.
/// </summary>
public bool IsCurrentUserOrganizer => CurrentEvent?.Attendees?.Any(a => a.IsOrganizer) ?? true;
#endregion
#region Show As Options
public ObservableCollection<ShowAsOption> ShowAsOptions { get; } = new ObservableCollection<ShowAsOption>();
[ObservableProperty]
public partial ShowAsOption SelectedShowAsOption { get; set; }
#endregion
#region RSVP Panel
[ObservableProperty]
public partial bool IsRsvpPanelVisible { get; set; }
public bool IncludeRsvpMessage => !string.IsNullOrEmpty(RsvpMessage);
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IncludeRsvpMessage))]
public partial string RsvpMessage { get; set; } = string.Empty;
public ObservableCollection<RsvpStatusOption> RsvpStatusOptions { get; } = new ObservableCollection<RsvpStatusOption>();
public CalendarItemStatus CurrentRsvpStatus
{
get
{
return CurrentEvent?.CalendarItem?.Status ?? CalendarItemStatus.NotResponded;
}
}
public string CurrentRsvpText
{
get
{
if (CurrentEvent?.CalendarItem == null) return Translator.CalendarEventResponse_Accept;
return CurrentEvent.CalendarItem.Status switch
{
CalendarItemStatus.Accepted => Translator.CalendarEventResponse_AcceptedResponse,
CalendarItemStatus.Tentative => Translator.CalendarEventResponse_TentativeResponse,
CalendarItemStatus.Cancelled => Translator.CalendarEventResponse_DeclinedResponse,
CalendarItemStatus.NotResponded => Translator.CalendarEventResponse_NotResponded,
_ => Translator.CalendarEventResponse_NotResponded
};
}
}
#endregion
public EventDetailsPageViewModel(ICalendarService calendarService,
INativeAppService nativeAppService,
IPreferencesService preferencesService,
IMailDialogService dialogService,
IWinoRequestDelegator winoRequestDelegator,
INavigationService navigationService,
INotificationBuilder notificationBuilder,
IUnderlyingThemeService underlyingThemeService,
IContactService contactService)
{
_calendarService = calendarService;
_nativeAppService = nativeAppService;
_preferencesService = preferencesService;
_dialogService = dialogService;
_winoRequestDelegator = winoRequestDelegator;
_navigationService = navigationService;
_underlyingThemeService = underlyingThemeService;
_notificationBuilder = notificationBuilder;
_contactService = contactService;
CurrentSettings = _preferencesService.GetCurrentCalendarSettings();
IsDarkWebviewRenderer = _underlyingThemeService.IsUnderlyingThemeDark();
foreach (var showAs in CalendarItemActionOptions.ShowAsOptions)
{
ShowAsOptions.Add(new ShowAsOption(showAs));
}
SelectedShowAsOption = ShowAsOptions.FirstOrDefault(option => option.ShowAs == CalendarItemShowAs.Busy) ?? ShowAsOptions.FirstOrDefault();
foreach (var responseStatus in CalendarItemActionOptions.ResponseOptions)
{
RsvpStatusOptions.Add(new RsvpStatusOption(responseStatus));
}
}
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
if (parameters == null || parameters is not CalendarItemTarget args)
return;
await LoadCalendarItemTargetAsync(args);
}
protected override async void OnCalendarItemUpdated(CalendarItem calendarItem, EntityUpdateSource source)
{
base.OnCalendarItemUpdated(calendarItem, source);
// If the current event was updated, reload it
if (IsCurrentEventMatch(calendarItem))
{
// Reflect client-side optimistic changes immediately; fallback to DB for server updates.
if (source == EntityUpdateSource.ClientUpdated || source == EntityUpdateSource.ClientReverted)
{
var previousAttendees = CurrentEvent?.Attendees?.ToList() ?? [];
CurrentEvent = new CalendarItemViewModel(calendarItem)
{
IsBusy = source == EntityUpdateSource.ClientUpdated
};
foreach (var attendee in previousAttendees)
{
CurrentEvent.Attendees.Add(attendee);
}
return;
}
// Refresh from DB when update comes from server sync.
var refreshedEvent = await _calendarService.GetCalendarItemAsync(calendarItem.Id);
if (refreshedEvent != null)
{
CurrentEvent = new CalendarItemViewModel(refreshedEvent);
await LoadAttendeesAsync(refreshedEvent.Id, refreshedEvent);
}
}
}
protected override async void OnCalendarItemAdded(CalendarItem calendarItem, EntityUpdateSource source)
{
base.OnCalendarItemAdded(calendarItem, source);
if (!IsCurrentEventMatch(calendarItem))
return;
if (source == EntityUpdateSource.ClientUpdated || source == EntityUpdateSource.ClientReverted)
{
CurrentEvent = new CalendarItemViewModel(calendarItem)
{
IsBusy = source == EntityUpdateSource.ClientUpdated
};
return;
}
var refreshedEvent = await _calendarService.GetCalendarItemAsync(calendarItem.Id);
if (refreshedEvent != null)
{
CurrentEvent = new CalendarItemViewModel(refreshedEvent);
await LoadAttendeesAsync(refreshedEvent.Id, refreshedEvent);
}
}
protected override void OnCalendarItemDeleted(CalendarItem calendarItem, EntityUpdateSource source)
{
base.OnCalendarItemDeleted(calendarItem, source);
// If the current event was deleted, navigate back
if (IsCurrentEventMatch(calendarItem))
{
NavigateBackToCalendar(forceReload: true);
}
}
private bool IsCurrentEventMatch(CalendarItem calendarItem)
{
if (CurrentEvent?.CalendarItem == null || calendarItem == null)
return false;
var trackedLocalItemId = calendarItem.RemoteEventId.GetClientTrackingId();
return CurrentEvent.CalendarItem.Id == calendarItem.Id ||
(trackedLocalItemId.HasValue && CurrentEvent.CalendarItem.Id == trackedLocalItemId.Value) ||
CurrentEvent.CalendarItem.RecurringCalendarItemId == calendarItem.Id;
}
private async Task LoadCalendarItemTargetAsync(CalendarItemTarget target)
{
try
{
var currentEventItem = await _calendarService.GetCalendarItemTargetAsync(target);
if (currentEventItem == null)
return;
CurrentEvent = new CalendarItemViewModel(currentEventItem);
await LoadAttendeesAsync(currentEventItem.Id, currentEventItem);
// Initialize SelectedShowAsOption based on current event's ShowAs
SelectedShowAsOption = ShowAsOptions.FirstOrDefault(o => o.ShowAs == currentEventItem.ShowAs) ?? ShowAsOptions[2];
// Load reminders for this calendar item
Reminders = await _calendarService.GetRemindersAsync(currentEventItem.Id);
InitializeReminderOptions();
// Load attachments
await LoadAttachmentsAsync(currentEventItem.Id);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
private async Task LoadAttendeesAsync(Guid calendarItemId, CalendarItem calendarItem)
{
var attendees = await _calendarService.GetAttendeesAsync(calendarItemId);
// Resolve contacts for all attendees in a single batch DB query.
var emails = attendees
.Where(a => !string.IsNullOrEmpty(a.Email))
.Select(a => a.Email)
.ToList();
if (!string.IsNullOrEmpty(calendarItem.OrganizerEmail))
emails.Add(calendarItem.OrganizerEmail);
var contacts = await _contactService.GetContactsByAddressesAsync(emails).ConfigureAwait(false);
var contactLookup = contacts.ToDictionary(c => c.Address, StringComparer.OrdinalIgnoreCase);
foreach (var attendee in attendees)
{
if (!string.IsNullOrEmpty(attendee.Email) && contactLookup.TryGetValue(attendee.Email, out var contact))
attendee.ResolvedContact = contact;
}
// Separate organizer from other attendees to ensure organizer is always first
var organizer = attendees.FirstOrDefault(a => a.IsOrganizer);
var nonOrganizerAttendees = attendees.Where(a => !a.IsOrganizer).ToList();
var attendeesForUi = new List<CalendarEventAttendee>();
// If the organizer is in the list, add them first
if (organizer != null)
{
attendeesForUi.Add(organizer);
}
else if (!string.IsNullOrEmpty(calendarItem.OrganizerEmail))
{
// If the organizer is not in the attendees list, create and add them first
var organizerAttendee = new CalendarEventAttendee
{
Id = Guid.NewGuid(),
CalendarItemId = calendarItem.Id,
Name = calendarItem.OrganizerDisplayName ?? calendarItem.OrganizerEmail,
Email = calendarItem.OrganizerEmail,
IsOrganizer = true,
AttendenceStatus = AttendeeStatus.Accepted
};
if (contactLookup.TryGetValue(calendarItem.OrganizerEmail, out var organizerContact))
organizerAttendee.ResolvedContact = organizerContact;
attendeesForUi.Add(organizerAttendee);
}
// Add all other attendees after the organizer
foreach (var item in nonOrganizerAttendees)
{
attendeesForUi.Add(item);
}
await ExecuteUIThread(() =>
{
if (CurrentEvent == null)
return;
CurrentEvent.Attendees.Clear();
foreach (var attendee in attendeesForUi)
{
CurrentEvent.Attendees.Add(attendee);
}
});
}
private async Task LoadAttachmentsAsync(Guid calendarItemId)
{
Attachments.Clear();
try
{
var attachments = await _calendarService.GetAttachmentsAsync(calendarItemId);
foreach (var attachment in attachments)
{
Attachments.Add(new CalendarAttachmentViewModel(attachment));
}
OnPropertyChanged(nameof(HasAttachments));
}
catch (Exception ex)
{
Debug.WriteLine($"Error loading attachments: {ex.Message}");
}
}
private void InitializeReminderOptions()
{
ReminderOptions.Clear();
// 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()
{
if (CurrentEvent == null) return;
if (CurrentEvent.AssignedCalendar?.IsReadOnly == true)
{
_dialogService.ShowReadOnlyCalendarMessage();
return;
}
try
{
// Capture original state BEFORE making any changes for potential revert
var originalItem = await _calendarService.GetCalendarItemAsync(CurrentEvent.CalendarItem.Id);
var originalAttendees = await _calendarService.GetAttendeesAsync(CurrentEvent.CalendarItem.Id);
// Get selected reminder options
var selectedOptions = ReminderOptions.Where(o => o.IsSelected).ToList();
// Create separate Reminder entities for each selected option
var newReminders = new List<Reminder>();
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.Id, newReminders);
Reminders = newReminders;
// Update ShowAs if changed
if (SelectedShowAsOption != null)
{
CurrentEvent.CalendarItem.ShowAs = SelectedShowAsOption.ShowAs;
}
// Update the calendar item and attendees in database
await _calendarService.UpdateCalendarItemAsync(CurrentEvent.CalendarItem, CurrentEvent.Attendees.ToList());
// Queue the update request to synchronizer with original state for revert capability
var preparationRequest = new CalendarOperationPreparationRequest(
CalendarSynchronizerOperation.UpdateEvent,
CurrentEvent.CalendarItem,
CurrentEvent.Attendees.ToList(),
ResponseMessage: null,
OriginalItem: originalItem,
OriginalAttendees: originalAttendees);
await _winoRequestDelegator.ExecuteAsync(preparationRequest);
NavigateBackToCalendar(forceReload: true);
}
catch (Exception ex)
{
Debug.WriteLine($"Error saving event: {ex.Message}");
_dialogService.InfoBarMessage(
Translator.Info_AttachmentSaveFailedTitle,
ex.Message,
InfoBarMessageType.Error);
}
}
[RelayCommand]
private async Task DeleteAsync()
{
if (CurrentEvent == null) return;
if (CurrentEvent.AssignedCalendar?.IsReadOnly == true)
{
_dialogService.ShowReadOnlyCalendarMessage();
return;
}
// If the event is a master recurring event, ask for confirmation
if (CurrentEvent.IsRecurringParent)
{
var confirmed = await _dialogService.ShowConfirmationDialogAsync(
Translator.DialogMessage_DeleteRecurringSeriesMessage,
Translator.DialogMessage_DeleteRecurringSeriesTitle,
Translator.Buttons_Delete);
if (!confirmed) return;
}
try
{
var preparationRequest = new CalendarOperationPreparationRequest(
CalendarSynchronizerOperation.DeleteEvent,
CurrentEvent.CalendarItem,
null);
await _winoRequestDelegator.ExecuteAsync(preparationRequest);
NavigateBackToCalendar(forceReload: true);
}
catch (Exception ex)
{
Debug.WriteLine($"Error deleting calendar event: {ex.Message}");
}
}
private void NavigateBackToCalendar(bool forceReload)
{
var navigationDate = CurrentEvent?.CalendarItem.LocalStartDate ?? DateTime.Now;
_navigationService.Navigate(
WinoPage.CalendarPage,
new CalendarPageNavigationArgs
{
NavigationDate = navigationDate,
ForceReload = forceReload
});
}
public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
{
if (args.Handled || args.Mode != WinoApplicationMode.Calendar || args.Action != KeyboardShortcutAction.Delete)
return;
await DeleteAsync();
args.Handled = true;
}
[RelayCommand]
private Task JoinOnlineAsync()
{
if (CurrentEvent == null || string.IsNullOrEmpty(CurrentEvent.CalendarItem.HtmlLink))
return Task.CompletedTask;
return _nativeAppService.LaunchUriAsync(new Uri(CurrentEvent.CalendarItem.HtmlLink));
}
[RelayCommand]
private Task CreateTestNotificationAsync()
{
if (CurrentEvent?.CalendarItem == null)
return Task.CompletedTask;
var reminderDurationInSeconds = Reminders?
.Where(x => x.DurationInSeconds > 0)
.OrderByDescending(x => x.DurationInSeconds)
.Select(x => x.DurationInSeconds)
.FirstOrDefault() ?? 0;
if (reminderDurationInSeconds <= 0)
reminderDurationInSeconds = Math.Max(_preferencesService.DefaultReminderDurationInSeconds, 30 * 60);
return _notificationBuilder.CreateCalendarReminderNotificationAsync(CurrentEvent.CalendarItem, reminderDurationInSeconds);
}
[RelayCommand]
private void ToggleRsvpPanel()
{
IsRsvpPanelVisible = !IsRsvpPanelVisible;
if (IsRsvpPanelVisible && CurrentEvent?.CalendarItem != null)
{
// Initialize selection based on current status
foreach (var item in RsvpStatusOptions)
{
item.IsSelected = CurrentEvent?.CalendarItem?.Status == item.Status;
}
}
}
[RelayCommand]
private void CloseRsvpPanel()
{
IsRsvpPanelVisible = false;
RsvpMessage = string.Empty;
}
[RelayCommand]
private async Task SendRsvpResponse(AttendeeStatus status)
{
if (CurrentEvent == null) return;
if (CurrentEvent.AssignedCalendar?.IsReadOnly == true)
{
_dialogService.ShowReadOnlyCalendarMessage();
return;
}
try
{
// Get the optional response message if user wants to include it
var responseMessage = IncludeRsvpMessage ? RsvpMessage : null;
// Map status to operation
CalendarSynchronizerOperation operation = status switch
{
AttendeeStatus.Accepted => CalendarSynchronizerOperation.AcceptEvent,
AttendeeStatus.Tentative => CalendarSynchronizerOperation.TentativeEvent,
AttendeeStatus.Declined => CalendarSynchronizerOperation.DeclineEvent,
_ => throw new InvalidOperationException($"Invalid RSVP status: {status}")
};
// Create preparation request with the optional message
var preparationRequest = new CalendarOperationPreparationRequest(
operation,
CurrentEvent.CalendarItem,
null,
responseMessage);
await _winoRequestDelegator.ExecuteAsync(preparationRequest);
// Reload attendees to get the updated status from the server
await LoadAttendeesAsync(CurrentEvent.CalendarItem.Id, CurrentEvent.CalendarItem);
OnPropertyChanged(nameof(CurrentRsvpText));
OnPropertyChanged(nameof(CurrentRsvpStatus));
CloseRsvpPanel();
}
catch (Exception ex)
{
Debug.WriteLine($"Error sending RSVP response: {ex.Message}");
_dialogService.InfoBarMessage(
Translator.Info_AttachmentSaveFailedTitle,
ex.Message,
InfoBarMessageType.Error);
}
}
[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}");
}
}
[RelayCommand]
private async Task OpenAttachmentAsync(CalendarAttachmentViewModel attachmentViewModel)
{
if (attachmentViewModel == null || CurrentEvent?.CalendarItem == null) return;
try
{
attachmentViewModel.IsBusy = true;
// If not downloaded, download it first
if (!attachmentViewModel.IsDownloaded)
{
await DownloadAttachmentAsync(attachmentViewModel);
}
// Launch the file
if (!string.IsNullOrEmpty(attachmentViewModel.Attachment.LocalFilePath) &&
File.Exists(attachmentViewModel.Attachment.LocalFilePath))
{
await _nativeAppService.LaunchFileAsync(attachmentViewModel.Attachment.LocalFilePath);
}
}
catch (Exception ex)
{
Log.Error(ex, "Failed to open calendar attachment.");
_dialogService.InfoBarMessage(
Translator.Info_AttachmentOpenFailedTitle,
Translator.Info_AttachmentOpenFailedMessage,
InfoBarMessageType.Error);
}
finally
{
attachmentViewModel.IsBusy = false;
}
}
[RelayCommand]
private async Task SaveAttachmentAsync(CalendarAttachmentViewModel attachmentViewModel)
{
if (attachmentViewModel == null) return;
try
{
attachmentViewModel.IsBusy = true;
var pickedPath = await _dialogService.PickWindowsFolderAsync();
if (string.IsNullOrEmpty(pickedPath)) return;
// Download if not already downloaded
if (!attachmentViewModel.IsDownloaded)
{
await DownloadAttachmentAsync(attachmentViewModel);
}
// Copy to selected location
if (!string.IsNullOrEmpty(attachmentViewModel.Attachment.LocalFilePath) &&
File.Exists(attachmentViewModel.Attachment.LocalFilePath))
{
var destinationPath = Path.Combine(pickedPath, attachmentViewModel.FileName);
File.Copy(attachmentViewModel.Attachment.LocalFilePath, destinationPath, overwrite: true);
_dialogService.InfoBarMessage(
Translator.Info_AttachmentSaveSuccessTitle,
Translator.Info_AttachmentSaveSuccessMessage,
InfoBarMessageType.Success);
}
}
catch (Exception ex)
{
Log.Error(ex, "Failed to save calendar attachment.");
_dialogService.InfoBarMessage(
Translator.Info_AttachmentSaveFailedTitle,
Translator.Info_AttachmentSaveFailedMessage,
InfoBarMessageType.Error);
}
finally
{
attachmentViewModel.IsBusy = false;
}
}
private async Task DownloadAttachmentAsync(CalendarAttachmentViewModel attachmentViewModel)
{
if (CurrentEvent?.CalendarItem == null) return;
// Create attachments folder for this calendar item
var attachmentsFolder = Path.Combine(
_nativeAppService.GetCalendarAttachmentsFolderPath(),
CurrentEvent.CalendarItem.Id.ToString());
Directory.CreateDirectory(attachmentsFolder);
var localFilePath = Path.Combine(attachmentsFolder, attachmentViewModel.FileName);
// Download attachment using synchronizer
await SynchronizationManager.Instance.DownloadCalendarAttachmentAsync(
CurrentEvent.CalendarItem,
attachmentViewModel.Attachment,
localFilePath);
// Mark as downloaded
await _calendarService.MarkAttachmentDownloadedAsync(
attachmentViewModel.Id,
localFilePath);
// Update view model
attachmentViewModel.Attachment.IsDownloaded = true;
attachmentViewModel.Attachment.LocalFilePath = localFilePath;
OnPropertyChanged(nameof(attachmentViewModel.IsDownloaded));
}
}
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;
}
}
public partial class ShowAsOption : ObservableObject
{
public CalendarItemShowAs ShowAs { get; }
public string DisplayText
{
get
{
return ShowAs switch
{
CalendarItemShowAs.Free => Translator.CalendarShowAs_Free,
CalendarItemShowAs.Tentative => Translator.CalendarShowAs_Tentative,
CalendarItemShowAs.Busy => Translator.CalendarShowAs_Busy,
CalendarItemShowAs.OutOfOffice => Translator.CalendarShowAs_OutOfOffice,
CalendarItemShowAs.WorkingElsewhere => Translator.CalendarShowAs_WorkingElsewhere,
_ => Translator.CalendarShowAs_Busy
};
}
}
public ShowAsOption(CalendarItemShowAs showAs)
{
ShowAs = showAs;
}
}
public partial class RsvpStatusOption : ObservableObject
{
public CalendarItemStatus Status { get; }
public string StatusText
{
get
{
return Status switch
{
CalendarItemStatus.Accepted => Translator.CalendarEventResponse_Accept,
CalendarItemStatus.Tentative => Translator.CalendarEventResponse_Tentative,
CalendarItemStatus.Cancelled => Translator.CalendarEventResponse_Decline,
_ => Translator.CalendarEventResponse_Accept
};
}
}
[ObservableProperty]
public partial bool IsSelected { get; set; }
public RsvpStatusOption(CalendarItemStatus status)
{
Status = status;
}
}
@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using CommunityToolkit.Mvvm.Collections;
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.ViewModels.Interfaces;
public interface IAccountCalendarStateService : INotifyPropertyChanged
{
IDispatcher Dispatcher { get; set; }
ReadOnlyObservableCollection<GroupedAccountCalendarViewModel> GroupedAccountCalendars { get; }
event EventHandler<GroupedAccountCalendarViewModel> CollectiveAccountGroupSelectionStateChanged;
event EventHandler<AccountCalendarViewModel> AccountCalendarSelectionStateChanged;
public void AddGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar);
public void RemoveGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar);
public void ClearGroupedAccountCalendars();
public void AddAccountCalendar(AccountCalendarViewModel accountCalendar);
public void RemoveAccountCalendar(AccountCalendarViewModel accountCalendar);
/// <summary>
/// Enumeration of currently selected calendars.
/// </summary>
IEnumerable<AccountCalendarViewModel> ActiveCalendars { get; }
IEnumerable<AccountCalendarViewModel> AllCalendars { get; }
bool IsAnySynchronizationInProgress { get; }
ReadOnlyObservableGroupedCollection<MailAccount, AccountCalendarViewModel> GroupedCalendars { get; set; }
}
@@ -0,0 +1,8 @@
using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.ViewModels.Messages;
public sealed record CalendarItemContextActionRequestedMessage(
CalendarItemViewModel CalendarItemViewModel,
CalendarContextMenuAction Action);
@@ -0,0 +1,13 @@
using Wino.Calendar.ViewModels.Data;
namespace Wino.Calendar.ViewModels.Messages;
public class CalendarItemDoubleTappedMessage
{
public CalendarItemDoubleTappedMessage(CalendarItemViewModel calendarItemViewModel)
{
CalendarItemViewModel = calendarItemViewModel;
}
public CalendarItemViewModel CalendarItemViewModel { get; }
}
@@ -0,0 +1,13 @@
using Wino.Calendar.ViewModels.Data;
namespace Wino.Calendar.ViewModels.Messages;
public class CalendarItemRightTappedMessage
{
public CalendarItemRightTappedMessage(CalendarItemViewModel calendarItemViewModel)
{
CalendarItemViewModel = calendarItemViewModel;
}
public CalendarItemViewModel CalendarItemViewModel { get; }
}
@@ -0,0 +1,12 @@
using Wino.Calendar.ViewModels.Data;
namespace Wino.Calendar.ViewModels.Messages;
public class CalendarItemTappedMessage
{
public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel)
{
CalendarItemViewModel = calendarItemViewModel;
}
public CalendarItemViewModel CalendarItemViewModel { get; }
}
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Platforms>x86;x64;arm64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<IsTrimmable>true</IsTrimmable>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TimePeriodLibrary.NET" />
<PackageReference Include="EmailValidation" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
<ProjectReference Include="..\Wino.Core.ViewModels\Wino.Core.ViewModels.csproj" />
<ProjectReference Include="..\Wino.Core\Wino.Core.csproj" />
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
<ProjectReference Include="..\Wino.Services\Wino.Services.csproj" />
</ItemGroup>
</Project>
-7
View File
@@ -1,7 +0,0 @@
<Application
x:Class="Wino.Calendar.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Wino.Calendar">
</Application>
-100
View File
@@ -1,100 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
namespace Wino.Calendar
{
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
sealed partial class App : Application
{
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
this.InitializeComponent();
this.Suspending += OnSuspending;
}
/// <summary>
/// Invoked when the application is launched normally by the end user. Other entry points
/// will be used such as when the application is launched to open a specific file.
/// </summary>
/// <param name="e">Details about the launch request and process.</param>
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
Frame rootFrame = Window.Current.Content as Frame;
// Do not repeat app initialization when the Window already has content,
// just ensure that the window is active
if (rootFrame == null)
{
// Create a Frame to act as the navigation context and navigate to the first page
rootFrame = new Frame();
rootFrame.NavigationFailed += OnNavigationFailed;
if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
{
//TODO: Load state from previously suspended application
}
// Place the frame in the current Window
Window.Current.Content = rootFrame;
}
if (e.PrelaunchActivated == false)
{
if (rootFrame.Content == null)
{
// When the navigation stack isn't restored navigate to the first page,
// configuring the new page by passing required information as a navigation
// parameter
rootFrame.Navigate(typeof(MainPage), e.Arguments);
}
// Ensure the current window is active
Window.Current.Activate();
}
}
/// <summary>
/// Invoked when Navigation to a certain page fails
/// </summary>
/// <param name="sender">The Frame which failed navigation</param>
/// <param name="e">Details about the navigation failure</param>
void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
{
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
}
/// <summary>
/// Invoked when application execution is being suspended. Application state is saved
/// without knowing whether the application will be terminated or resumed with the contents
/// of memory still intact.
/// </summary>
/// <param name="sender">The source of the suspend request.</param>
/// <param name="e">Details about the suspend request.</param>
private void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();
//TODO: Save application state and stop any background activity
deferral.Complete();
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

-14
View File
@@ -1,14 +0,0 @@
<Page
x:Class="Wino.Calendar.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Wino.Calendar"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid>
</Grid>
</Page>
-30
View File
@@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409
namespace Wino.Calendar
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
}
}
-58
View File
@@ -1,58 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
IgnorableNamespaces="uap mp">
<!-- Publisher Cache Folders -->
<Extensions>
<Extension Category="windows.publisherCacheFolders">
<PublisherCacheFolders>
<Folder Name="WinoShared" />
</PublisherCacheFolders>
</Extension>
</Extensions>
<Identity
Name="58272BurakKSE.WinoCalendar"
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
Version="1.0.0.0" />
<mp:PhoneIdentity PhoneProductId="f047b7dd-96ec-4d54-a862-9321e271e449" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>Wino Calendar</DisplayName>
<PublisherDisplayName>Burak KÖSE</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.0.0" MaxVersionTested="10.0.0.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="Wino.Calendar.App">
<uap:VisualElements
DisplayName="Wino Calendar"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png"
Description="Wino.Calendar"
BackgroundColor="transparent">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png"/>
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<Capability Name="internetClient" />
</Capabilities>
</Package>
-29
View File
@@ -1,29 +0,0 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Wino.Calendar")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Wino.Calendar")]
[assembly: AssemblyCopyright("Copyright © 2023")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: ComVisible(false)]
-31
View File
@@ -1,31 +0,0 @@
<!--
This file contains Runtime Directives used by .NET Native. The defaults here are suitable for most
developers. However, you can modify these parameters to modify the behavior of the .NET Native
optimizer.
Runtime Directives are documented at https://go.microsoft.com/fwlink/?LinkID=391919
To fully enable reflection for App1.MyClass and all of its public/private members
<Type Name="App1.MyClass" Dynamic="Required All"/>
To enable dynamic creation of the specific instantiation of AppClass<T> over System.Int32
<TypeInstantiation Name="App1.AppClass" Arguments="System.Int32" Activate="Required Public" />
Using the Namespace directive to apply reflection policy to all the types in a particular namespace
<Namespace Name="DataClasses.ViewModels" Serialize="All" />
-->
<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
<Application>
<!--
An Assembly element with Name="*Application*" applies to all assemblies in
the application package. The asterisks are not wildcards.
-->
<Assembly Name="*Application*" Dynamic="Required All" />
<!-- Add your application specific runtime directives here. -->
</Application>
</Directives>
-172
View File
@@ -1,172 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
<ProjectGuid>{600F4979-DB7E-409D-B7DA-B60BE4C55C35}</ProjectGuid>
<OutputType>AppContainerExe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Wino.Calendar</RootNamespace>
<AssemblyName>Wino.Calendar</AssemblyName>
<DefaultLanguage>en-US</DefaultLanguage>
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
<TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.22621.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<MinimumVisualStudioVersion>14</MinimumVisualStudioVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WindowsXamlEnableOverview>true</WindowsXamlEnableOverview>
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
<PackageCertificateThumbprint>125A5273FCFE8D551C3FED87F67C87A663E98F1B</PackageCertificateThumbprint>
<PackageCertificateKeyFile />
<GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x86\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<PlatformTarget>x86</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\x86\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x86</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
<Prefer32Bit>true</Prefer32Bit>
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\ARM\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<PlatformTarget>ARM</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM'">
<OutputPath>bin\ARM\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<PlatformTarget>ARM</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
<Prefer32Bit>true</Prefer32Bit>
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\ARM64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<PlatformTarget>ARM64</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
<Prefer32Bit>true</Prefer32Bit>
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM64'">
<OutputPath>bin\ARM64\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<PlatformTarget>ARM64</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
<Prefer32Bit>true</Prefer32Bit>
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
<Prefer32Bit>true</Prefer32Bit>
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
</PropertyGroup>
<PropertyGroup>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
</PropertyGroup>
<ItemGroup>
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
</Compile>
<Compile Include="MainPage.xaml.cs">
<DependentUpon>MainPage.xaml</DependentUpon>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<AppxManifest Include="Package.appxmanifest">
<SubType>Designer</SubType>
</AppxManifest>
</ItemGroup>
<ItemGroup>
<None Include="Package.StoreAssociation.xml" />
<Content Include="Properties\Default.rd.xml" />
<Content Include="Assets\LockScreenLogo.scale-200.png" />
<Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\Square150x150Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Page Include="MainPage.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
<Version>6.2.14</Version>
</PackageReference>
</ItemGroup>
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '14.0' ">
<VisualStudioVersion>14.0</VisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>
+10
View File
@@ -0,0 +1,10 @@
namespace Wino.Core.Domain;
public static class AppUrls
{
public const string Website = "https://www.winomail.app";
public const string Discord = "https://discord.gg/windows-apps-hub-714581497222398064";
public const string GitHub = "https://github.com/bkaankose/Wino-Mail/";
public const string PrivacyPolicy = "https://www.winomail.app/support/privacy";
public const string Paypal = "https://paypal.me/bkaankose?country.x=PL&locale.x=en_US";
}
+14
View File
@@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Wino.Core.Domain.Models.Updates;
namespace Wino.Core.Domain;
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(UpdateNotes))]
[JsonSerializable(typeof(List<UpdateNoteSection>))]
public partial class BasicTypesJsonContext : JsonSerializerContext;
@@ -0,0 +1,23 @@
using System.Collections.Generic;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain;
public static class CalendarItemActionOptions
{
public static IReadOnlyList<CalendarItemShowAs> ShowAsOptions { get; } =
[
CalendarItemShowAs.Free,
CalendarItemShowAs.Tentative,
CalendarItemShowAs.Busy,
CalendarItemShowAs.OutOfOffice,
CalendarItemShowAs.WorkingElsewhere
];
public static IReadOnlyList<CalendarItemStatus> ResponseOptions { get; } =
[
CalendarItemStatus.Accepted,
CalendarItemStatus.Tentative,
CalendarItemStatus.Cancelled
];
}
@@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Core.Domain;
public static class CalendarRecurrenceSummaryFormatter
{
private static readonly DayOfWeek[] OrderedDays =
[
DayOfWeek.Monday,
DayOfWeek.Tuesday,
DayOfWeek.Wednesday,
DayOfWeek.Thursday,
DayOfWeek.Friday,
DayOfWeek.Saturday,
DayOfWeek.Sunday
];
public static string BuildSummary(
bool isRecurring,
DateTimeOffset effectiveStart,
DateTimeOffset effectiveEnd,
bool isAllDay,
CalendarSettings settings,
int interval,
CalendarItemRecurrenceFrequency frequency,
IReadOnlyCollection<DayOfWeek> daysOfWeek,
DateTimeOffset? recurrenceEndDate)
{
var culture = settings?.CultureInfo ?? CultureInfo.CurrentCulture;
var timeSummary = isAllDay
? Translator.CalendarItemAllDay
: string.Format(
culture,
Translator.CalendarEventCompose_TimeRangeSummary,
effectiveStart.ToString(settings?.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour ? "HH:mm" : "h:mm tt", culture),
effectiveEnd.ToString(settings?.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour ? "HH:mm" : "h:mm tt", culture));
if (!isRecurring)
{
return string.Format(
culture,
Translator.CalendarEventCompose_SingleOccurrenceSummary,
effectiveStart.ToString("dddd yyyy-MM-dd", culture),
timeSummary);
}
var normalizedDays = NormalizeDays(daysOfWeek);
var isEveryDay = (frequency == CalendarItemRecurrenceFrequency.Daily && interval == 1) ||
(frequency == CalendarItemRecurrenceFrequency.Weekly && interval == 1 && normalizedDays.Count == 7);
var cadenceSummary = isEveryDay
? $"{Translator.CalendarEventCompose_Every} {Translator.CalendarEventCompose_FrequencyDay}"
: interval == 1
? $"{Translator.CalendarEventCompose_Every} {GetSingularFrequencyLabel(frequency)}"
: $"{Translator.CalendarEventCompose_Every} {interval.ToString(culture)} {GetPluralFrequencyLabel(frequency)}";
var weekdaySummary = string.Empty;
if (frequency == CalendarItemRecurrenceFrequency.Weekly && normalizedDays.Count > 0 && normalizedDays.Count < 7)
{
weekdaySummary = string.Format(
culture,
Translator.CalendarEventCompose_WeekdaySummary,
string.Join(", ", normalizedDays.Select(day => culture.DateTimeFormat.GetDayName(day))));
}
var untilSummary = recurrenceEndDate.HasValue
? string.Format(
culture,
Translator.CalendarEventCompose_UntilSummary,
recurrenceEndDate.Value.ToString("ddd yyyy-MM-dd", culture))
: string.Empty;
return string.Format(
culture,
Translator.CalendarEventCompose_RecurringSummarySmart,
cadenceSummary,
weekdaySummary,
timeSummary,
effectiveStart.ToString("dddd yyyy-MM-dd", culture),
untilSummary).Trim();
}
private static IReadOnlyList<DayOfWeek> NormalizeDays(IReadOnlyCollection<DayOfWeek> daysOfWeek)
{
if (daysOfWeek == null || daysOfWeek.Count == 0)
{
return [];
}
return daysOfWeek
.Distinct()
.OrderBy(day => Array.IndexOf(OrderedDays, day))
.ToList();
}
private static string GetSingularFrequencyLabel(CalendarItemRecurrenceFrequency frequency)
{
return frequency switch
{
CalendarItemRecurrenceFrequency.Daily => Translator.CalendarEventCompose_FrequencyDay,
CalendarItemRecurrenceFrequency.Weekly => Translator.CalendarEventCompose_FrequencyWeek,
CalendarItemRecurrenceFrequency.Monthly => Translator.CalendarEventCompose_FrequencyMonth,
CalendarItemRecurrenceFrequency.Yearly => Translator.CalendarEventCompose_FrequencyYear,
_ => Translator.CalendarEventCompose_FrequencyWeek
};
}
private static string GetPluralFrequencyLabel(CalendarItemRecurrenceFrequency frequency)
{
return frequency switch
{
CalendarItemRecurrenceFrequency.Daily => Translator.CalendarEventCompose_FrequencyDayPlural,
CalendarItemRecurrenceFrequency.Weekly => Translator.CalendarEventCompose_FrequencyWeekPlural,
CalendarItemRecurrenceFrequency.Monthly => Translator.CalendarEventCompose_FrequencyMonthPlural,
CalendarItemRecurrenceFrequency.Yearly => Translator.CalendarEventCompose_FrequencyYearPlural,
_ => Translator.CalendarEventCompose_FrequencyWeekPlural
};
}
}
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Wino.Core.Domain;
public static class CalendarReminderSnoozeOptions
{
private static readonly int[] SupportedSnoozeMinutes = [5, 10, 15, 30];
public static IReadOnlyList<int> GetSupportedSnoozeMinutes()
=> SupportedSnoozeMinutes;
public static IReadOnlyList<int> GetAllowedSnoozeMinutes(long reminderDurationInSeconds, long defaultReminderDurationInSeconds)
{
var reminderMinutes = (int)Math.Max(0, reminderDurationInSeconds / 60);
if (reminderMinutes <= 0)
return [];
var maxSnoozeMinutes = reminderMinutes;
var defaultReminderMinutes = (int)Math.Max(0, defaultReminderDurationInSeconds / 60);
if (defaultReminderMinutes > 0)
maxSnoozeMinutes = Math.Min(maxSnoozeMinutes, defaultReminderMinutes);
return SupportedSnoozeMinutes.Where(minutes => minutes <= maxSnoozeMinutes).ToArray();
}
}
@@ -0,0 +1,219 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Itenso.TimePeriod;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Core.Domain.Collections;
public class CalendarEventCollection
{
public event EventHandler<ICalendarItem> CalendarItemAdded;
public event EventHandler<ICalendarItem> CalendarItemRemoved;
public event EventHandler<ICalendarItem> CalendarItemUpdated;
public event EventHandler CalendarItemsCleared;
private ObservableRangeCollection<ICalendarItem> _internalRegularEvents = [];
private ObservableRangeCollection<ICalendarItem> _internalAllDayEvents = [];
public ReadOnlyObservableCollection<ICalendarItem> RegularEvents { get; }
public ReadOnlyObservableCollection<ICalendarItem> AllDayEvents { get; } // TODO: Rename this to include multi-day events.
public ITimePeriod Period { get; }
public CalendarSettings Settings { get; }
private readonly List<ICalendarItem> _allItems = new List<ICalendarItem>();
public CalendarEventCollection(ITimePeriod period, CalendarSettings settings)
{
Period = period;
Settings = settings;
RegularEvents = new ReadOnlyObservableCollection<ICalendarItem>(_internalRegularEvents);
AllDayEvents = new ReadOnlyObservableCollection<ICalendarItem>(_internalAllDayEvents);
}
public bool HasCalendarEvent(AccountCalendar accountCalendar)
=> _allItems.Any(x => x.AssignedCalendar.Id == accountCalendar.Id);
public ICalendarItem GetCalendarItem(Guid calendarItemId)
{
return _allItems.FirstOrDefault(x => x.Id == calendarItemId);
}
public void ClearSelectionStates()
{
foreach (var item in _allItems)
{
if (item is ICalendarItemViewModel calendarItemViewModel)
{
calendarItemViewModel.IsSelected = false;
}
}
}
public void FilterByCalendars(IEnumerable<Guid> visibleCalendarIds)
{
foreach (var item in _allItems)
{
var collections = GetProperCollectionsForCalendarItem(item);
foreach (var collection in collections)
{
if (!visibleCalendarIds.Contains(item.AssignedCalendar.Id) && collection.Contains(item))
{
RemoveCalendarItemInternal(collection, item, false);
}
else if (visibleCalendarIds.Contains(item.AssignedCalendar.Id) && !collection.Contains(item))
{
AddCalendarItemInternal(collection, item, false);
}
}
}
}
private IEnumerable<ObservableRangeCollection<ICalendarItem>> GetProperCollectionsForCalendarItem(ICalendarItem calendarItem)
{
// All-day events go to all days.
// Multi-day events go to both.
// Anything else goes to regular.
if (calendarItem.IsAllDayEvent)
{
return [_internalAllDayEvents];
}
else if (calendarItem.IsMultiDayEvent)
{
return [_internalRegularEvents, _internalAllDayEvents];
}
else
{
return [_internalRegularEvents];
}
}
public void AddCalendarItem(ICalendarItem calendarItem)
{
var collections = GetProperCollectionsForCalendarItem(calendarItem);
foreach (var collection in collections)
{
AddCalendarItemInternal(collection, calendarItem);
}
}
public void RemoveCalendarItem(ICalendarItem calendarItem)
{
var collections = GetProperCollectionsForCalendarItem(calendarItem);
foreach (var collection in collections)
{
RemoveCalendarItemInternal(collection, calendarItem);
}
}
public void RemoveCalendarItems(Func<ICalendarItem, bool> predicate)
{
if (predicate == null) return;
var itemsToRemove = _allItems.Where(predicate).ToList();
foreach (var item in itemsToRemove)
{
RemoveCalendarItem(item);
}
}
private void AddCalendarItemInternal(ObservableRangeCollection<ICalendarItem> collection, ICalendarItem calendarItem, bool create = true)
{
if (calendarItem is not ICalendarItemViewModel viewModel)
throw new ArgumentException("CalendarItem must be of type ICalendarItemViewModel", nameof(calendarItem));
// Set the displaying context for proper title calculation
viewModel.DisplayingPeriod = Period;
viewModel.CalendarSettings = Settings;
collection.Add(calendarItem);
if (create)
{
_allItems.Add(calendarItem);
}
CalendarItemAdded?.Invoke(this, calendarItem);
}
private void RemoveCalendarItemInternal(ObservableRangeCollection<ICalendarItem> collection, ICalendarItem calendarItem, bool destroy = true)
{
if (calendarItem is not ICalendarItemViewModel)
throw new ArgumentException("CalendarItem must be of type ICalendarItemViewModel", nameof(calendarItem));
collection.Remove(calendarItem);
if (destroy)
{
_allItems.Remove(calendarItem);
}
CalendarItemRemoved?.Invoke(this, calendarItem);
}
/// <summary>
/// Updates an existing calendar item in-place. If the item's type changed (all-day vs regular),
/// it will be moved to the appropriate collection.
/// </summary>
/// <param name="calendarItem">The updated calendar item data.</param>
/// <returns>True if the item was found and updated; false otherwise.</returns>
public bool UpdateCalendarItem(CalendarItem calendarItem)
{
var existingItem = _allItems.FirstOrDefault(x => x.Id == calendarItem.Id);
if (existingItem == null)
return false;
// Get the collections this item is currently in (before update)
var oldCollections = GetProperCollectionsForCalendarItem(existingItem).ToList();
// Update the underlying data
if (existingItem is ICalendarItemViewModel viewModel)
{
viewModel.UpdateFrom(calendarItem);
}
// Get the collections this item should be in (after update)
var newCollections = GetProperCollectionsForCalendarItem(existingItem).ToList();
// Check if the collections changed
var collectionsToRemoveFrom = oldCollections.Except(newCollections).ToList();
var collectionsToAddTo = newCollections.Except(oldCollections).ToList();
// Remove from old collections that are no longer applicable
foreach (var collection in collectionsToRemoveFrom)
{
collection.Remove(existingItem);
}
// Add to new collections that are now applicable
foreach (var collection in collectionsToAddTo)
{
if (!collection.Contains(existingItem))
{
collection.Add(existingItem);
}
}
CalendarItemUpdated?.Invoke(this, existingItem);
return true;
}
public void Clear()
{
_internalAllDayEvents.Clear();
_internalRegularEvents.Clear();
_allItems.Clear();
CalendarItemsCleared?.Invoke(this, EventArgs.Empty);
}
}
@@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace Wino.Core.Domain.Collections;
/// <summary>
/// Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed.
/// </summary>
/// <typeparam name="T"></typeparam>
public class ObservableRangeCollection<T> : ObservableCollection<T>
{
/// <summary>
/// Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class.
/// </summary>
public ObservableRangeCollection()
: base()
{
}
/// <summary>
/// Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class that contains elements copied from the specified collection.
/// </summary>
/// <param name="collection">collection: The collection from which the elements are copied.</param>
/// <exception cref="ArgumentNullException">The collection parameter cannot be null.</exception>
public ObservableRangeCollection(IEnumerable<T> collection)
: base(collection)
{
}
/// <summary>
/// Adds the elements of the specified collection to the end of the ObservableCollection(Of T).
/// </summary>
public void AddRange(IEnumerable<T> collection, NotifyCollectionChangedAction notificationMode = NotifyCollectionChangedAction.Add)
{
if (notificationMode != NotifyCollectionChangedAction.Add && notificationMode != NotifyCollectionChangedAction.Reset)
throw new ArgumentException("Mode must be either Add or Reset for AddRange.", nameof(notificationMode));
if (collection == null)
throw new ArgumentNullException(nameof(collection));
CheckReentrancy();
var startIndex = Count;
var itemsAdded = AddArrangeCore(collection);
if (!itemsAdded)
return;
if (notificationMode == NotifyCollectionChangedAction.Reset)
{
RaiseChangeNotificationEvents(action: NotifyCollectionChangedAction.Reset);
return;
}
var changedItems = collection is List<T> ? (List<T>)collection : new List<T>(collection);
RaiseChangeNotificationEvents(
action: NotifyCollectionChangedAction.Add,
changedItems: changedItems,
startingIndex: startIndex);
}
/// <summary>
/// Removes the first occurence of each item in the specified collection from ObservableCollection(Of T). NOTE: with notificationMode = Remove, removed items starting index is not set because items are not guaranteed to be consecutive.
/// </summary>
public void RemoveRange(IEnumerable<T> collection, NotifyCollectionChangedAction notificationMode = NotifyCollectionChangedAction.Reset)
{
if (notificationMode != NotifyCollectionChangedAction.Remove && notificationMode != NotifyCollectionChangedAction.Reset)
throw new ArgumentException("Mode must be either Remove or Reset for RemoveRange.", nameof(notificationMode));
if (collection == null)
throw new ArgumentNullException(nameof(collection));
CheckReentrancy();
if (notificationMode == NotifyCollectionChangedAction.Reset)
{
var raiseEvents = false;
foreach (var item in collection)
{
Items.Remove(item);
raiseEvents = true;
}
if (raiseEvents)
RaiseChangeNotificationEvents(action: NotifyCollectionChangedAction.Reset);
return;
}
var changedItems = new List<T>(collection);
for (var i = 0; i < changedItems.Count; i++)
{
if (!Items.Remove(changedItems[i]))
{
changedItems.RemoveAt(i); //Can't use a foreach because changedItems is intended to be (carefully) modified
i--;
}
}
if (changedItems.Count == 0)
return;
RaiseChangeNotificationEvents(
action: NotifyCollectionChangedAction.Remove,
changedItems: changedItems);
}
/// <summary>
/// Clears the current collection and replaces it with the specified item.
/// </summary>
public void Replace(T item) => ReplaceRange(new T[] { item });
/// <summary>
/// Clears the current collection and replaces it with the specified collection.
/// </summary>
public void ReplaceRange(IEnumerable<T> collection)
{
if (collection == null)
throw new ArgumentNullException(nameof(collection));
CheckReentrancy();
var previouslyEmpty = Items.Count == 0;
Items.Clear();
AddArrangeCore(collection);
var currentlyEmpty = Items.Count == 0;
if (previouslyEmpty && currentlyEmpty)
return;
RaiseChangeNotificationEvents(action: NotifyCollectionChangedAction.Reset);
}
public void InsertRange(IEnumerable<T> items)
{
CheckReentrancy();
foreach (var item in items)
Items.Insert(0, item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
private bool AddArrangeCore(IEnumerable<T> collection)
{
var itemAdded = false;
foreach (var item in collection)
{
Items.Add(item);
itemAdded = true;
}
return itemAdded;
}
private void RaiseChangeNotificationEvents(NotifyCollectionChangedAction action, List<T> changedItems = null, int startingIndex = -1)
{
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
if (changedItems is null)
OnCollectionChanged(new NotifyCollectionChangedEventArgs(action));
else
OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, changedItems: changedItems, startingIndex: startingIndex));
}
}
+33 -13
View File
@@ -1,17 +1,37 @@
namespace Wino.Core.Domain
namespace Wino.Core.Domain;
public static class Constants
{
public static class Constants
{
/// <summary>
/// MIME header that exists in all the drafts created from Wino.
/// </summary>
public const string WinoLocalDraftHeader = "X-Wino-Draft-Id";
public const string LocalDraftStartPrefix = "localDraft_";
/// <summary>
/// MIME header that exists in all the drafts created from Wino.
/// </summary>
public const string WinoLocalDraftHeader = "X-Wino-Draft-Id";
public const string DispositionNotificationToHeader = "Disposition-Notification-To";
public const string OriginalMessageIdHeader = "Original-Message-ID";
public const string LocalDraftStartPrefix = "localDraft_";
public const string ToastMailUniqueIdKey = nameof(ToastMailUniqueIdKey);
public const string ToastActionKey = nameof(ToastActionKey);
public const string CalendarEventRecurrenceRuleSeperator = "___";
public const string ClientLogFile = "Client_.log";
public const string ServerLogFile = "Server_.log";
}
public const string ToastMailUniqueIdKey = nameof(ToastMailUniqueIdKey);
public const string ToastActionKey = nameof(ToastActionKey);
public const string ToastMailAccountIdKey = nameof(ToastMailAccountIdKey);
public const string ToastCalendarItemIdKey = nameof(ToastCalendarItemIdKey);
public const string ToastCalendarActionKey = nameof(ToastCalendarActionKey);
public const string ToastCalendarNavigateAction = nameof(ToastCalendarNavigateAction);
public const string ToastCalendarJoinOnlineAction = nameof(ToastCalendarJoinOnlineAction);
public const string ToastCalendarSnoozeAction = nameof(ToastCalendarSnoozeAction);
public const string ToastCalendarSnoozeDurationInputId = nameof(ToastCalendarSnoozeDurationInputId);
public const string ToastModeKey = nameof(ToastModeKey);
public const string ToastModeMail = nameof(ToastModeMail);
public const string ToastModeCalendar = nameof(ToastModeCalendar);
public const string ToastDismissActionKey = nameof(ToastDismissActionKey);
public const string ToastStoreUpdateActionKey = nameof(ToastStoreUpdateActionKey);
public const string ToastStoreUpdateActionInstall = nameof(ToastStoreUpdateActionInstall);
public const string ClientLogFile = "Client_.log";
public const string ServerLogFile = "Server_.log";
public const string LogArchiveFileName = "WinoLogs.zip";
public const string WinoMailIdentiifer = nameof(WinoMailIdentiifer);
public const string WinoCalendarIdentifier = nameof(WinoCalendarIdentifier);
}
@@ -1,17 +0,0 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities
{
public class AccountSignature
{
[PrimaryKey]
public Guid Id { get; set; }
public string Name { get; set; }
public string HtmlBody { get; set; }
public Guid MailAccountId { get; set; }
}
}
@@ -1,53 +0,0 @@
using SQLite;
using System;
using System.Collections.Generic;
namespace Wino.Core.Domain.Entities
{
/// <summary>
/// Back storage for simple name-address book.
/// These values will be inserted during MIME fetch.
/// </summary>
// TODO: This can easily evolve to Contact store, just like People app in Windows 10/11.
// Do it.
public class AddressInformation : IEquatable<AddressInformation>
{
[PrimaryKey]
public string Address { get; set; }
public string Name { get; set; }
public string DisplayName => Address == Name ? Address : $"{Name} <{Address}>";
public override bool Equals(object obj)
{
return Equals(obj as AddressInformation);
}
public bool Equals(AddressInformation other)
{
return !(other is null) &&
Address == other.Address &&
Name == other.Name;
}
public override int GetHashCode()
{
int hashCode = -1717786383;
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Address);
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Name);
return hashCode;
}
public static bool operator ==(AddressInformation left, AddressInformation right)
{
return EqualityComparer<AddressInformation>.Default.Equals(left, right);
}
public static bool operator !=(AddressInformation left, AddressInformation right)
{
return !(left == right);
}
}
}
@@ -0,0 +1,34 @@
using System;
using SQLite;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Entities.Calendar;
[Preserve]
public class AccountCalendar : IAccountCalendar
{
[PrimaryKey]
public Guid Id { get; set; }
public Guid AccountId { get; set; }
public string RemoteCalendarId { get; set; }
public string SynchronizationDeltaToken { get; set; }
public string Name { get; set; }
public bool IsPrimary { get; set; }
public bool IsReadOnly { get; set; }
public bool IsSynchronizationEnabled { get; set; } = true;
public bool IsExtended { get; set; } = true;
public CalendarItemShowAs DefaultShowAs { get; set; } = CalendarItemShowAs.Busy;
/// <summary>
/// Unused for now.
/// </summary>
public string TextColorHex { get; set; }
public string BackgroundColorHex { get; set; }
public bool IsBackgroundColorUserOverridden { get; set; }
public string TimeZone { get; set; }
[Ignore]
public MailAccount MailAccount { get; set; }
}
@@ -0,0 +1,55 @@
using System;
using SQLite;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities.Calendar;
/// <summary>
/// Represents metadata for calendar event attachments.
/// Actual file content is downloaded on-demand.
/// </summary>
public class CalendarAttachment
{
[PrimaryKey]
public Guid Id { get; set; }
/// <summary>
/// The calendar item this attachment belongs to.
/// </summary>
public Guid CalendarItemId { get; set; }
/// <summary>
/// Remote identifier for the attachment from the provider (Outlook, Gmail, etc.).
/// </summary>
public string RemoteAttachmentId { get; set; }
/// <summary>
/// File name of the attachment.
/// </summary>
public string FileName { get; set; }
/// <summary>
/// Size of the attachment in bytes.
/// </summary>
public long Size { get; set; }
/// <summary>
/// MIME content type (e.g., "application/pdf", "image/png").
/// </summary>
public string ContentType { get; set; }
/// <summary>
/// Whether the attachment has been downloaded to local storage.
/// </summary>
public bool IsDownloaded { get; set; }
/// <summary>
/// Local file path where the attachment is stored (if downloaded).
/// </summary>
public string LocalFilePath { get; set; }
/// <summary>
/// When the attachment was last modified.
/// </summary>
public DateTimeOffset LastModified { get; set; }
}
@@ -0,0 +1,26 @@
using System;
using SQLite;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities.Calendar;
public class CalendarEventAttendee
{
[PrimaryKey]
public Guid Id { get; set; }
public Guid CalendarItemId { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public AttendeeStatus AttendenceStatus { get; set; }
public bool IsOrganizer { get; set; }
public bool IsOptionalAttendee { get; set; }
public string Comment { get; set; }
/// <summary>
/// Resolved contact from the contact store. Populated at runtime via IContactService;
/// not persisted to the database.
/// </summary>
[Ignore]
public AccountContact ResolvedContact { get; set; }
}
@@ -0,0 +1,209 @@
using System;
using System.Diagnostics;
using Itenso.TimePeriod;
using SQLite;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Core.Domain.Entities.Calendar;
[DebuggerDisplay("{Title} ({StartDate} - {EndDate})")]
public class CalendarItem : ICalendarItem
{
[PrimaryKey]
public Guid Id { get; set; }
public string RemoteEventId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Location { get; set; }
/// <summary>
/// Indicates whether this item is a local preview that hasn't been synced to the server yet.
/// When true, the item exists only in the local database without a RemoteEventId.
/// Used to prevent duplicates when the server returns the newly created event.
/// </summary>
[Ignore]
public bool IsLocalPreview => string.IsNullOrEmpty(RemoteEventId);
public DateTime StartDate { get; set; }
public DateTime EndDate
{
get
{
return StartDate.AddSeconds(DurationInSeconds);
}
}
/// <summary>
/// IANA timezone identifier for the start time (e.g., "America/New_York", "Europe/London").
/// If null or empty, UTC is assumed.
/// </summary>
public string StartTimeZone { get; set; }
/// <summary>
/// IANA timezone identifier for the end time (e.g., "America/New_York", "Europe/London").
/// If null or empty, UTC is assumed.
/// </summary>
public string EndTimeZone { get; set; }
private ITimePeriod _period;
public ITimePeriod Period
{
get
{
_period ??= new TimeRange(StartDate, EndDate);
return _period;
}
}
/// <summary>
/// Events that starts at midnight and ends at midnight are considered all-day events.
/// </summary>
public bool IsAllDayEvent
{
get
{
return
StartDate.TimeOfDay == TimeSpan.Zero &&
EndDate.TimeOfDay == TimeSpan.Zero;
}
}
/// <summary>
/// Events that are child instances of a recurring event (occurrences or exceptions).
/// </summary>
public bool IsRecurringChild
{
get
{
return RecurringCalendarItemId != null;
}
}
/// <summary>
/// Events that are part of a recurring series (either as parent or child).
/// </summary>
public bool IsRecurringEvent => IsRecurringChild || IsRecurringParent;
/// <summary>
/// Events that are the master event definition of recurrence events.
/// </summary>
public bool IsRecurringParent
{
get
{
return !string.IsNullOrEmpty(Recurrence) && RecurringCalendarItemId == null;
}
}
/// <summary>
/// Events that are not all-day events and last more than one day are considered multi-day events.
/// </summary>
public bool IsMultiDayEvent
{
get
{
return Period.Duration.TotalDays >= 1 && !IsAllDayEvent;
}
}
public double DurationInSeconds { get; set; }
public string Recurrence { get; set; }
public string OrganizerDisplayName { get; set; }
public string OrganizerEmail { get; set; }
/// <summary>
/// The id of the parent calendar item of the recurring event.
/// Exceptional instances are stored as a separate calendar item.
/// This makes the calendar item a child of the recurring event.
/// </summary>
public Guid? RecurringCalendarItemId { get; set; }
/// <summary>
/// Indicates read-only events. Default is false.
/// </summary>
public bool IsLocked { get; set; }
/// <summary>
/// Hidden events must not be displayed to the user.
/// This usually happens when a child instance of recurring parent is cancelled after creation.
/// </summary>
public bool IsHidden { get; set; }
// TODO
public string CustomEventColorHex { get; set; }
public string HtmlLink { get; set; }
public DateTime? SnoozedUntil { get; set; }
public CalendarItemStatus Status { get; set; }
public CalendarItemVisibility Visibility { get; set; }
/// <summary>
/// Indicates how the event should be shown in the calendar (Free, Busy, Tentative, etc.).
/// </summary>
public CalendarItemShowAs ShowAs { get; set; } = CalendarItemShowAs.Busy;
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
public Guid CalendarId { get; set; }
[Ignore]
public IAccountCalendar AssignedCalendar { get; set; }
[Ignore]
public bool CanChangeStartAndEndDate
{
get
{
if (IsLocked)
{
return false;
}
var accountAddress = AssignedCalendar?.MailAccount?.Address;
return string.IsNullOrWhiteSpace(OrganizerEmail) ||
string.IsNullOrWhiteSpace(accountAddress) ||
string.Equals(OrganizerEmail, accountAddress, StringComparison.OrdinalIgnoreCase);
}
}
/// <summary>
/// Id to load information related to this event (attendees, reminders, etc.).
/// For child events, if they have their own data, use their own Id.
/// For events that share data with their parent, return parent's Id.
/// </summary>
public Guid EventTrackingId => Id;
/// <summary>
/// Gets the start date converted to user's local timezone for display.
/// StartDate is stored according to StartTimeZone.
/// </summary>
[Ignore]
public DateTime LocalStartDate
{
get
{
return this.GetLocalStartDate();
}
}
/// <summary>
/// Gets the end date converted to user's local timezone for display.
/// EndDate is calculated from StartDate and is in StartTimeZone.
/// </summary>
[Ignore]
public DateTime LocalEndDate
{
get
{
return this.GetLocalEndDate();
}
}
public string GetDisplayTitle(ITimePeriod displayingPeriod, CalendarSettings calendarSettings) => Period.ToString();
}
@@ -0,0 +1,19 @@
using System;
using SQLite;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities.Calendar;
public class Reminder
{
[PrimaryKey]
public Guid Id { get; set; }
public Guid CalendarItemId { get; set; }
/// <summary>
/// Duration in seconds before the event start time when the reminder should trigger.
/// For example, 900 seconds = 15 minutes before event.
/// </summary>
public long DurationInSeconds { get; set; }
public CalendarItemReminderType ReminderType { get; set; }
}
@@ -1,53 +0,0 @@
using System;
using SQLite;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities
{
public class CustomServerInformation
{
[PrimaryKey]
public Guid Id { get; set; }
public Guid AccountId { get; set; }
/// <summary>
/// This field is ignored. DisplayName is stored in MailAccount as SenderName from now.
/// </summary>
[Ignore]
public string DisplayName { get; set; }
public string Address { get; set; }
public string IncomingServer { get; set; }
public string IncomingServerUsername { get; set; }
public string IncomingServerPassword { get; set; }
public string IncomingServerPort { get; set; }
public CustomIncomingServerType IncomingServerType { get; set; }
public string OutgoingServer { get; set; }
public string OutgoingServerPort { get; set; }
public string OutgoingServerUsername { get; set; }
public string OutgoingServerPassword { get; set; }
/// <summary>
/// useSSL True: SslOnConnect
/// useSSL False: StartTlsWhenAvailable
/// </summary>
public ImapConnectionSecurity IncomingServerSocketOption { get; set; }
public ImapAuthenticationMethod IncomingAuthenticationMethod { get; set; }
public ImapConnectionSecurity OutgoingServerSocketOption { get; set; }
public ImapAuthenticationMethod OutgoingAuthenticationMethod { get; set; }
public string ProxyServer { get; set; }
public string ProxyServerPort { get; set; }
/// <summary>
/// Number of concurrent clients that can connect to the server.
/// Default is 5.
/// </summary>
public int MaxConcurrentClients { get; set; }
}
}
@@ -0,0 +1,16 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities.Mail;
public class AccountSignature
{
[PrimaryKey]
public Guid Id { get; set; }
public string Name { get; set; }
public string HtmlBody { get; set; }
public Guid MailAccountId { get; set; }
}
@@ -0,0 +1,16 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities.Mail;
public class EmailTemplate
{
[PrimaryKey]
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string HtmlContent { get; set; } = string.Empty;
}
@@ -0,0 +1,109 @@
using System;
using System.Collections.ObjectModel;
using System.Security.Cryptography.X509Certificates;
using SQLite;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities.Mail;
public class RemoteAccountAlias
{
/// <summary>
/// Display address of the alias.
/// </summary>
public string AliasAddress { get; set; }
/// <summary>
/// Address to be included in Reply-To header when alias is used for sending messages.
/// </summary>
public string ReplyToAddress { get; set; }
/// <summary>
/// Whether this alias is the primary alias for the account.
/// </summary>
public bool IsPrimary { get; set; }
/// <summary>
/// Whether the alias is verified by the server.
/// Only Gmail aliases are verified for now.
/// Non-verified alias messages might be rejected by SMTP server.
/// </summary>
public bool IsVerified { get; set; }
/// <summary>
/// Whether this alias is the root alias for the account.
/// Root alias means the first alias that was created for the account.
/// It can't be deleted or changed.
/// </summary>
public bool IsRootAlias { get; set; }
/// <summary>
/// Optional sender name for the alias.
/// Falls back to account's sender name if not set when preparing messages.
/// Used for Gmail only.
/// </summary>
public string AliasSenderName { get; set; }
/// <summary>
/// Whether the alias was entered by the user or discovered from the provider.
/// </summary>
public AliasSource Source { get; set; } = AliasSource.Manual;
/// <summary>
/// Represents Wino's confidence that the alias can be used for sending.
/// </summary>
public AliasSendCapability SendCapability { get; set; } = AliasSendCapability.Unknown;
}
public class MailAccountAlias : RemoteAccountAlias
{
/// <summary>
/// Unique Id for the alias.
/// </summary>
[PrimaryKey]
public Guid Id { get; set; }
/// <summary>
/// Account id that this alias is attached to.
/// </summary>
public Guid AccountId { get; set; }
/// <summary>
/// Root aliases can't be deleted.
/// </summary>
public bool CanDelete => !IsRootAlias;
public string SelectedSigningCertificateThumbprint { get; set; }
public bool IsSmimeEncryptionEnabled { get; set; }
[Ignore]
public X509Certificate2 SelectedSigningCertificate { get; set; }
[Ignore]
public ObservableCollection<X509Certificate2> Certificates { get; set; } = [];
[Ignore]
public bool IsCapabilityConfirmed => SendCapability == AliasSendCapability.Confirmed;
[Ignore]
public bool IsCapabilityUnknown => SendCapability == AliasSendCapability.Unknown;
[Ignore]
public bool IsCapabilityDenied => SendCapability == AliasSendCapability.Denied;
[Ignore]
public string CapabilityDisplayName => SendCapability switch
{
AliasSendCapability.Confirmed => Translator.AccountAlias_Status_Confirmed,
AliasSendCapability.Denied => Translator.AccountAlias_Status_Denied,
_ => Translator.AccountAlias_Status_Unknown
};
[Ignore]
public string SourceDisplayName => Source switch
{
AliasSource.ProviderDiscovered => Translator.AccountAlias_Source_ProviderDiscovered,
_ => Translator.AccountAlias_Source_Manual
};
}
@@ -0,0 +1,25 @@
using System;
using SQLite;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities.Mail;
public class MailCategory
{
[PrimaryKey]
public Guid Id { get; set; }
public Guid MailAccountId { get; set; }
public string RemoteId { get; set; }
public string Name { get; set; }
public bool IsFavorite { get; set; }
public string BackgroundColorHex { get; set; }
public string TextColorHex { get; set; }
public MailCategorySource Source { get; set; } = MailCategorySource.Local;
}
@@ -0,0 +1,14 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities.Mail;
public class MailCategoryAssignment
{
[PrimaryKey]
public Guid Id { get; set; }
public Guid MailCategoryId { get; set; }
public Guid MailCopyUniqueId { get; set; }
}
+172
View File
@@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using SQLite;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities.Mail;
/// <summary>
/// Summary of the parsed MIME messages.
/// Wino will do non-network operations on this table and others from the original MIME.
/// </summary>
public class MailCopy
{
/// <summary>
/// Unique Id of the mail.
/// </summary>
[PrimaryKey]
public Guid UniqueId { get; set; }
/// <summary>
/// Not unique id of the item. Some operations held on this Id, some on the UniqueId.
/// Same message can be in different folder. In that case UniqueId is used.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Folder that this mail belongs to.
/// </summary>
public Guid FolderId { get; set; }
/// <summary>
/// Conversation id for the mail.
/// </summary>
public string ThreadId { get; set; }
/// <summary>
/// MIME MessageId if exists.
/// </summary>
public string MessageId { get; set; }
/// <summary>
/// References header from MIME
/// </summary>
public string References { get; set; }
/// <summary>
/// In-Reply-To header from MIME
/// </summary>
public string InReplyTo { get; set; }
/// <summary>
/// Name for the sender.
/// </summary>
public string FromName { get; set; }
/// <summary>
/// Address of the sender.
/// </summary>
public string FromAddress { get; set; }
/// <summary>
/// Subject of the mail.
/// </summary>
public string Subject { get; set; }
/// <summary>
/// Short preview of the content.
/// </summary>
public string PreviewText { get; set; }
/// <summary>
/// Date that represents this mail has been created in provider servers.
/// Stored always in UTC.
/// </summary>
public DateTime CreationDate { get; set; }
/// <summary>
/// Importance of the mail.
/// </summary>
public MailImportance Importance { get; set; }
/// <summary>
/// Read status for the mail.
/// </summary>
public bool IsRead { get; set; }
/// <summary>
/// Flag status.
/// Flagged for Outlook.
/// Important for Gmail.
/// </summary>
public bool IsFlagged { get; set; }
/// <summary>
/// To support Outlook.
/// Gmail doesn't use it.
/// </summary>
public bool IsFocused { get; set; }
/// <summary>
/// Whether mail has attachments included or not.
/// </summary>
public bool HasAttachments { get; set; }
/// <summary>
/// Type of mail item (regular mail, calendar invitation, calendar response, etc.).
/// </summary>
public MailItemType ItemType { get; set; } = MailItemType.Mail;
/// <summary>
/// Assigned draft id.
/// </summary>
public string DraftId { get; set; }
/// <summary>
/// Whether this mail is only created locally.
/// </summary>
[Ignore]
public bool IsLocalDraft => !string.IsNullOrEmpty(DraftId) && DraftId.StartsWith(Constants.LocalDraftStartPrefix);
/// <summary>
/// Whether this copy is draft or not.
/// </summary>
public bool IsDraft { get; set; }
/// <summary>
/// File id that this mail is assigned to.
/// This Id is immutable. It's used to find the file in the file system.
/// Even after mapping local draft to remote draft, it will not change.
/// </summary>
public Guid FileId { get; set; }
/// <summary>
/// Folder that this mail is assigned to.
/// Warning: This field is not populated by queries.
/// Services or View Models are responsible for populating this field.
/// </summary>
[Ignore]
public MailItemFolder AssignedFolder { get; set; }
/// <summary>
/// Account that this mail is assigned to.
/// Warning: This field is not populated by queries.
/// Services or View Models are responsible for populating this field.
/// </summary>
[Ignore]
public MailAccount AssignedAccount { get; set; }
/// <summary>
/// Contact information of the sender if exists.
/// Warning: This field is not populated by queries.
/// Services or View Models are responsible for populating this field.
/// </summary>
[Ignore]
public AccountContact SenderContact { get; set; }
[Ignore]
public bool IsReadReceiptRequested { get; set; }
[Ignore]
public SentMailReceiptStatus ReadReceiptStatus { get; set; }
[Ignore]
public DateTime? ReadReceiptAcknowledgedAtUtc { get; set; }
[Ignore]
public Guid? ReadReceiptMessageUniqueId { get; set; }
public IEnumerable<Guid> GetContainingIds() => [UniqueId];
public override string ToString() => $"{Subject} <-> {Id}";
}
@@ -0,0 +1,31 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities.Mail;
/// <summary>
/// Maps a calendar invitation mail item to a persisted calendar event.
/// </summary>
public class MailInvitationCalendarMapping
{
[PrimaryKey]
public Guid Id { get; set; }
public Guid AccountId { get; set; }
/// <summary>
/// MailCopy.Id value of the invitation mail.
/// </summary>
public string MailCopyId { get; set; }
/// <summary>
/// iCalendar UID extracted from invitation MIME/ICS content.
/// </summary>
public string InvitationUid { get; set; }
public Guid CalendarId { get; set; }
public Guid CalendarItemId { get; set; }
public string CalendarRemoteEventId { get; set; }
public DateTime UpdatedAtUtc { get; set; } = DateTime.UtcNow;
}
@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using SQLite;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Folders;
namespace Wino.Core.Domain.Entities.Mail;
[DebuggerDisplay("{FolderName} - {SpecialFolderType}")]
public class MailItemFolder : IMailItemFolder
{
[PrimaryKey]
public Guid Id { get; set; }
public string RemoteFolderId { get; set; }
public string ParentRemoteFolderId { get; set; }
public Guid MailAccountId { get; set; }
public string FolderName { get; set; }
public SpecialFolderType SpecialFolderType { get; set; }
public bool IsSystemFolder { get; set; }
public bool IsSticky { get; set; }
public bool IsSynchronizationEnabled { get; set; }
public bool IsHidden { get; set; }
public bool ShowUnreadCount { get; set; }
// User-defined ordering within its navigation section (Pinned / Categories / More).
// 0 means "no custom order set" — the folder falls back to the default sort
// (alphabetic for More, canonical SpecialFolderType order as a tiebreak for Pinned).
public int Order { get; set; }
public DateTime? LastSynchronizedDate { get; set; }
// For IMAP
public uint UidValidity { get; set; }
public long HighestModeSeq { get; set; }
public uint HighestKnownUid { get; set; }
public DateTime? LastUidReconcileUtc { get; set; }
/// <summary>
/// Outlook shares delta changes per-folder. Gmail is for per-account.
/// This is only used for Outlook provider.
/// </summary>
public string DeltaToken { get; set; }
// For GMail Labels
public string TextColorHex { get; set; }
public string BackgroundColorHex { get; set; }
[Ignore]
public List<IMailItemFolder> ChildFolders { get; set; } = [];
// Category and Move type folders are not valid move targets.
// These folders are virtual. They don't exist on the server.
public bool IsMoveTarget => !(SpecialFolderType == SpecialFolderType.More || SpecialFolderType == SpecialFolderType.Category);
public bool ContainsSpecialFolderType(SpecialFolderType type)
{
if (SpecialFolderType == type)
return true;
foreach (var child in ChildFolders)
{
if (child.SpecialFolderType == type)
{
return true;
}
else
{
return child.ContainsSpecialFolderType(type);
}
}
return false;
}
public static MailItemFolder CreateMoreFolder() => new MailItemFolder() { IsSticky = true, SpecialFolderType = SpecialFolderType.More, FolderName = Translator.MoreFolderNameOverride };
public static MailItemFolder CreateCategoriesFolder() => new MailItemFolder() { IsSticky = true, SpecialFolderType = SpecialFolderType.Category, FolderName = Translator.CategoriesFolderNameOverride };
public override string ToString() => FolderName;
}
@@ -0,0 +1,12 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities.Mail;
public class MergedInbox
{
[PrimaryKey]
public Guid Id { get; set; }
public string Name { get; set; }
}
@@ -0,0 +1,25 @@
using System;
using SQLite;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities.Mail;
public class SentMailReceiptState
{
[PrimaryKey]
public Guid MailUniqueId { get; set; }
public Guid AccountId { get; set; }
public string MessageId { get; set; }
public bool IsReceiptRequested { get; set; }
public DateTime RequestedAtUtc { get; set; }
public SentMailReceiptStatus Status { get; set; }
public DateTime? AcknowledgedAtUtc { get; set; }
public Guid? ReceiptMessageUniqueId { get; set; }
}
-82
View File
@@ -1,82 +0,0 @@
using System;
using SQLite;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities
{
public class MailAccount
{
[PrimaryKey]
public Guid Id { get; set; }
/// <summary>
/// Given name of the account in Wino.
/// </summary>
public string Name { get; set; }
/// <summary>
/// TODO: Display name of the authenticated user/account.
/// API integrations will query this value from the API.
/// IMAP is populated by user on setup dialog.
/// </summary>
public string SenderName { get; set; }
/// <summary>
/// Account e-mail address.
/// </summary>
public string Address { get; set; }
/// <summary>
/// Provider type of the account. Outlook,Gmail etc...
/// </summary>
public MailProviderType ProviderType { get; set; }
/// <summary>
/// For tracking change delta.
/// Gmail : historyId
/// Outlook: deltaToken
/// </summary>
public string SynchronizationDeltaIdentifier { get; set; }
/// <summary>
/// TODO: Gets or sets the custom account identifier color in hex.
/// </summary>
public string AccountColorHex { get; set; }
/// <summary>
/// Gets or sets the listing order of the account in the accounts list.
/// </summary>
public int Order { get; set; }
/// <summary>
/// Gets or sets whether the account has any reason for an interactive user action to fix continue operating.
/// </summary>
public AccountAttentionReason AttentionReason { get; set; }
/// <summary>
/// Gets or sets the id of the merged inbox this account belongs to.
/// </summary>
public Guid? MergedInboxId { get; set; }
/// <summary>
/// Contains the merged inbox this account belongs to.
/// Ignored for all SQLite operations.
/// </summary>
[Ignore]
public MergedInbox MergedInbox { get; set; }
/// <summary>
/// Populated only when account has custom server information.
/// </summary>
[Ignore]
public CustomServerInformation ServerInformation { get; set; }
/// <summary>
/// Account preferences.
/// </summary>
[Ignore]
public MailAccountPreferences Preferences { get; set; }
}
}
@@ -1,49 +0,0 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities
{
public class MailAccountPreferences
{
[PrimaryKey]
public Guid Id { get; set; }
/// <summary>
/// Id of the account in MailAccount table.
/// </summary>
public Guid AccountId { get; set; }
/// <summary>
/// Gets or sets whether sent draft messages should be appended to the sent folder.
/// Some IMAP servers do this automatically, some don't.
/// It's disabled by default.
/// </summary>
public bool ShouldAppendMessagesToSentFolder { get; set; }
/// <summary>
/// Gets or sets whether the notifications are enabled for the account.
/// </summary>
public bool IsNotificationsEnabled { get; set; }
/// <summary>
/// Gets or sets whether the account has Focused inbox support.
/// Null if the account provider type doesn't support Focused inbox.
/// </summary>
public bool? IsFocusedInboxEnabled { get; set; }
/// <summary>
/// Gets or sets whether signature should be appended automatically.
/// </summary>
public bool IsSignatureEnabled { get; set; }
/// <summary>
/// Gets or sets signature for new messages. Null if signature is not needed.
/// </summary>
public Guid? SignatureIdForNewMessages { get; set; }
/// <summary>
/// Gets or sets signature for following messages. Null if signature is not needed.
/// </summary>
public Guid? SignatureIdForFollowingMessages { get; set; }
}
}
-147
View File
@@ -1,147 +0,0 @@
using System;
using System.Collections.Generic;
using SQLite;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.MailItem;
namespace Wino.Core.Domain.Entities
{
/// <summary>
/// Summary of the parsed MIME messages.
/// Wino will do non-network operations on this table and others from the original MIME.
/// </summary>
public class MailCopy : IMailItem
{
/// <summary>
/// Unique Id of the mail.
/// </summary>
[PrimaryKey]
public Guid UniqueId { get; set; }
/// <summary>
/// Not unique id of the item. Some operations held on this Id, some on the UniqueId.
/// Same message can be in different folder. In that case UniqueId is used.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Folder that this mail belongs to.
/// </summary>
public Guid FolderId { get; set; }
/// <summary>
/// Conversation id for the mail.
/// </summary>
public string ThreadId { get; set; }
/// <summary>
/// MIME MessageId if exists.
/// </summary>
public string MessageId { get; set; }
/// <summary>
/// References header from MIME
/// </summary>
public string References { get; set; }
/// <summary>
/// In-Reply-To header from MIME
/// </summary>
public string InReplyTo { get; set; }
/// <summary>
/// Name for the sender.
/// </summary>
public string FromName { get; set; }
/// <summary>
/// Address of the sender.
/// </summary>
public string FromAddress { get; set; }
/// <summary>
/// Subject of the mail.
/// </summary>
public string Subject { get; set; }
/// <summary>
/// Short preview of the content.
/// </summary>
public string PreviewText { get; set; }
/// <summary>
/// Date that represents this mail has been created in provider servers.
/// Stored always in UTC.
/// </summary>
public DateTime CreationDate { get; set; }
/// <summary>
/// Importance of the mail.
/// </summary>
public MailImportance Importance { get; set; }
/// <summary>
/// Read status for the mail.
/// </summary>
public bool IsRead { get; set; }
/// <summary>
/// Flag status.
/// Flagged for Outlook.
/// Important for Gmail.
/// </summary>
public bool IsFlagged { get; set; }
/// <summary>
/// To support Outlook.
/// Gmail doesn't use it.
/// </summary>
public bool IsFocused { get; set; }
/// <summary>
/// Whether mail has attachments included or not.
/// </summary>
public bool HasAttachments { get; set; }
/// <summary>
/// Assigned draft id.
/// </summary>
public string DraftId { get; set; }
/// <summary>
/// Whether this mail is only created locally.
/// </summary>
[Ignore]
public bool IsLocalDraft => !string.IsNullOrEmpty(DraftId) && DraftId.StartsWith(Constants.LocalDraftStartPrefix);
/// <summary>
/// Whether this copy is draft or not.
/// </summary>
public bool IsDraft { get; set; }
/// <summary>
/// File id that this mail is assigned to.
/// This Id is immutable. It's used to find the file in the file system.
/// Even after mapping local draft to remote draft, it will not change.
/// </summary>
public Guid FileId { get; set; }
/// <summary>
/// Folder that this mail is assigned to.
/// Warning: This field is not populated by queries.
/// Services or View Models are responsible for populating this field.
/// </summary>
[Ignore]
public MailItemFolder AssignedFolder { get; set; }
/// <summary>
/// Account that this mail is assigned to.
/// Warning: This field is not populated by queries.
/// Services or View Models are responsible for populating this field.
/// </summary>
[Ignore]
public MailAccount AssignedAccount { get; set; }
public IEnumerable<Guid> GetContainingIds() => new[] { UniqueId };
public override string ToString() => $"{Subject} <-> {Id}";
}
}
@@ -1,75 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using SQLite;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Folders;
namespace Wino.Core.Domain.Entities
{
[DebuggerDisplay("{FolderName} - {SpecialFolderType}")]
public class MailItemFolder : IMailItemFolder
{
[PrimaryKey]
public Guid Id { get; set; }
public string RemoteFolderId { get; set; }
public string ParentRemoteFolderId { get; set; }
public Guid MailAccountId { get; set; }
public string FolderName { get; set; }
public SpecialFolderType SpecialFolderType { get; set; }
public bool IsSystemFolder { get; set; }
public bool IsSticky { get; set; }
public bool IsSynchronizationEnabled { get; set; }
public bool IsHidden { get; set; }
public bool ShowUnreadCount { get; set; }
public DateTime? LastSynchronizedDate { get; set; }
// For IMAP
public uint UidValidity { get; set; }
public long HighestModeSeq { get; set; }
/// <summary>
/// Outlook shares delta changes per-folder. Gmail is for per-account.
/// This is only used for Outlook provider.
/// </summary>
public string DeltaToken { get; set; }
// For GMail Labels
public string TextColorHex { get; set; }
public string BackgroundColorHex { get; set; }
[Ignore]
public List<IMailItemFolder> ChildFolders { get; set; } = [];
// Category and Move type folders are not valid move targets.
// These folders are virtual. They don't exist on the server.
public bool IsMoveTarget => !(SpecialFolderType == SpecialFolderType.More || SpecialFolderType == SpecialFolderType.Category);
public bool ContainsSpecialFolderType(SpecialFolderType type)
{
if (SpecialFolderType == type)
return true;
foreach (var child in ChildFolders)
{
if (child.SpecialFolderType == type)
{
return true;
}
else
{
return child.ContainsSpecialFolderType(type);
}
}
return false;
}
public static MailItemFolder CreateMoreFolder() => new MailItemFolder() { IsSticky = true, SpecialFolderType = SpecialFolderType.More, FolderName = Translator.MoreFolderNameOverride };
public static MailItemFolder CreateCategoriesFolder() => new MailItemFolder() { IsSticky = true, SpecialFolderType = SpecialFolderType.Category, FolderName = Translator.CategoriesFolderNameOverride };
public override string ToString() => FolderName;
}
}
-13
View File
@@ -1,13 +0,0 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities
{
public class MergedInbox
{
[PrimaryKey]
public Guid Id { get; set; }
public string Name { get; set; }
}
}
@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using SQLite;
namespace Wino.Core.Domain.Entities.Shared;
/// <summary>
/// Back storage for simple name-address book.
/// These values will be inserted during MIME fetch.
/// </summary>
// TODO: This can easily evolve to Contact store, just like People app in Windows 10/11.
// Do it.
public class AccountContact : IEquatable<AccountContact>, IContactDisplayItem
{
/// <summary>
/// E-mail address of the contact.
/// </summary>
[PrimaryKey]
public string Address { get; set; }
/// <summary>
/// Display name of the contact.
/// </summary>
public string Name { get; set; }
/// <summary>
/// File ID for the contact picture stored on disk.
/// The actual file lives at {ApplicationDataFolderPath}/contacts/{ContactPictureFileId}.jpg.
/// </summary>
public Guid? ContactPictureFileId { get; set; }
/// <summary>
/// All registered accounts have their contacts registered as root.
/// Root contacts must not be overridden by any configuration.
/// They are created on account creation.
/// </summary>
public bool IsRootContact { get; set; }
/// <summary>
/// When true, indicates that the contact has been manually modified by the user.
/// Contacts with this flag set to true should not be updated during synchronization.
/// </summary>
public bool IsOverridden { get; set; } = false;
public string DisplayName => string.IsNullOrWhiteSpace(Name) ? Address : Name;
AccountContact IContactDisplayItem.PreviewContact => this;
public override bool Equals(object obj)
{
return Equals(obj as AccountContact);
}
public bool Equals(AccountContact other)
{
return other is not null &&
Address == other.Address &&
Name == other.Name;
}
public override int GetHashCode()
{
return HashCode.Combine(Address, Name);
}
public static bool operator ==(AccountContact left, AccountContact right)
{
return EqualityComparer<AccountContact>.Default.Equals(left, right);
}
public static bool operator !=(AccountContact left, AccountContact right)
{
return !(left == right);
}
}
@@ -0,0 +1,19 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities.Shared;
/// <summary>
/// A named group of contacts that can be expanded to individual addresses during mail composition.
/// </summary>
public class ContactGroup
{
[PrimaryKey]
public Guid Id { get; set; }
/// <summary>Display name of the group (e.g., "Team Alpha", "Family").</summary>
public string Name { get; set; }
/// <summary>Optional description for the group.</summary>
public string Description { get; set; }
}
@@ -0,0 +1,21 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities.Shared;
/// <summary>
/// Associates an e-mail address with a <see cref="ContactGroup"/>.
/// </summary>
public class ContactGroupMember
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
/// <summary>Group this member belongs to.</summary>
[Indexed]
public Guid GroupId { get; set; }
/// <summary>E-mail address of the member (FK to AccountContact.Address).</summary>
[Indexed]
public string MemberAddress { get; set; }
}
@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using SQLite;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities.Shared;
public class CustomServerInformation
{
[PrimaryKey]
public Guid Id { get; set; }
public Guid AccountId { get; set; }
/// <summary>
/// This field is ignored. DisplayName is stored in MailAccount as SenderName from now.
/// </summary>
[Ignore]
public string DisplayName { get; set; }
public string Address { get; set; }
public string IncomingServer { get; set; }
public string IncomingServerUsername { get; set; }
public string IncomingServerPassword { get; set; }
public string IncomingServerPort { get; set; }
public CustomIncomingServerType IncomingServerType { get; set; }
public string OutgoingServer { get; set; }
public string OutgoingServerPort { get; set; }
public string OutgoingServerUsername { get; set; }
public string OutgoingServerPassword { get; set; }
public string CalDavServiceUrl { get; set; }
public string CalDavUsername { get; set; }
public string CalDavPassword { get; set; }
public ImapCalendarSupportMode CalendarSupportMode { get; set; }
/// <summary>
/// useSSL True: SslOnConnect
/// useSSL False: StartTlsWhenAvailable
/// </summary>
public ImapConnectionSecurity IncomingServerSocketOption { get; set; }
public ImapAuthenticationMethod IncomingAuthenticationMethod { get; set; }
public ImapConnectionSecurity OutgoingServerSocketOption { get; set; }
public ImapAuthenticationMethod OutgoingAuthenticationMethod { get; set; }
public string ProxyServer { get; set; }
public string ProxyServerPort { get; set; }
/// <summary>
/// Number of concurrent clients that can connect to the server.
/// Default is 5.
/// </summary>
public int MaxConcurrentClients { get; set; }
public Dictionary<string, string> GetConnectionProperties()
{
// Printout the public connection properties.
var connectionProperties = new Dictionary<string, string>
{
{ "IncomingServer", IncomingServer },
{ "IncomingServerPort", IncomingServerPort },
{ "IncomingServerSocketOption", IncomingServerSocketOption.ToString() },
{ "IncomingAuthenticationMethod", IncomingAuthenticationMethod.ToString() },
{ "OutgoingServer", OutgoingServer },
{ "OutgoingServerPort", OutgoingServerPort },
{ "OutgoingServerSocketOption", OutgoingServerSocketOption.ToString() },
{ "OutgoingAuthenticationMethod", OutgoingAuthenticationMethod.ToString() },
{ "CalendarSupportMode", CalendarSupportMode.ToString() },
{ "CalDavServiceUrl", CalDavServiceUrl },
{ "ProxyServer", ProxyServer },
{ "ProxyServerPort", ProxyServerPort }
};
return connectionProperties;
}
}
@@ -0,0 +1,8 @@
namespace Wino.Core.Domain.Entities.Shared;
public interface IContactDisplayItem
{
string DisplayName { get; }
string Address { get; }
AccountContact PreviewContact { get; }
}
@@ -0,0 +1,65 @@
using System;
using SQLite;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities.Shared;
/// <summary>
/// Represents a user-defined keyboard shortcut for mail operations.
/// </summary>
public class KeyboardShortcut
{
[PrimaryKey]
public Guid Id { get; set; }
/// <summary>
/// The application mode this shortcut applies to.
/// </summary>
public WinoApplicationMode Mode { get; set; } = WinoApplicationMode.Mail;
/// <summary>
/// The key combination string (e.g., "D", "Delete", "F1").
/// </summary>
public string Key { get; set; }
/// <summary>
/// The modifier keys for this shortcut.
/// </summary>
public ModifierKeys ModifierKeys { get; set; }
/// <summary>
/// The shortcut action this shortcut triggers.
/// </summary>
public KeyboardShortcutAction Action { get; set; }
/// <summary>
/// Whether this shortcut is enabled.
/// </summary>
public bool IsEnabled { get; set; } = true;
/// <summary>
/// When this shortcut was created.
/// </summary>
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
/// <summary>
/// User-friendly display name for the shortcut.
/// </summary>
public string DisplayName
{
get
{
var modifierText = string.Empty;
if (ModifierKeys.HasFlag(ModifierKeys.Control))
modifierText += "Ctrl+";
if (ModifierKeys.HasFlag(ModifierKeys.Alt))
modifierText += "Alt+";
if (ModifierKeys.HasFlag(ModifierKeys.Shift))
modifierText += "Shift+";
if (ModifierKeys.HasFlag(ModifierKeys.Windows))
modifierText += "Win+";
return modifierText + Key;
}
}
}
@@ -0,0 +1,148 @@
using System;
using SQLite;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities.Shared;
public class MailAccount
{
[PrimaryKey]
public Guid Id { get; set; }
/// <summary>
/// Given name of the account in Wino.
/// </summary>
public string Name { get; set; }
/// <summary>
/// TODO: Display name of the authenticated user/account.
/// API integrations will query this value from the API.
/// IMAP is populated by user on setup dialog.
/// </summary>
public string SenderName { get; set; }
/// <summary>
/// Account e-mail address.
/// </summary>
public string Address { get; set; }
/// <summary>
/// Provider type of the account. Outlook,Gmail etc...
/// </summary>
public MailProviderType ProviderType { get; set; }
/// <summary>
/// For tracking mail change delta.
/// Gmail : historyId
/// Outlook: deltaToken
/// </summary>
public string SynchronizationDeltaIdentifier { get; set; }
/// <summary>
/// For tracking calendar change delta.
/// Gmail: It's per-calendar, so unused.
/// Outlook: deltaLink
/// </summary>
public string CalendarSynchronizationDeltaIdentifier { get; set; }
/// <summary>
/// TODO: Gets or sets the custom account identifier color in hex.
/// </summary>
public string AccountColorHex { get; set; }
/// <summary>
/// Base64 encoded profile picture of the account.
/// </summary>
public string Base64ProfilePictureData { get; set; }
/// <summary>
/// Gets or sets the listing order of the account in the accounts list.
/// </summary>
public int Order { get; set; }
/// <summary>
/// Gets or sets whether the account has any reason for an interactive user action to fix continue operating.
/// </summary>
public AccountAttentionReason AttentionReason { get; set; }
/// <summary>
/// Gets or sets the id of the merged inbox this account belongs to.
/// </summary>
public Guid? MergedInboxId { get; set; }
/// <summary>
/// Gets or sets the additional IMAP provider assignment for the account.
/// Providers that use IMAP as a synchronizer but have special requirements.
/// </summary>
public SpecialImapProvider SpecialImapProvider { get; set; }
/// <summary>
/// Gets or sets whether mail access is granted for this account.
/// When false, mail folders, aliases, compose flows, and mail synchronization are unavailable.
/// Default is true for legacy accounts to preserve existing behavior.
/// </summary>
public bool IsMailAccessGranted { get; set; } = true;
/// <summary>
/// Gets or sets whether calendar access is granted for this account.
/// When false, synchronizers will not process EventMessages or calendar invitations.
/// Default is false for existing accounts to prevent scope issues.
/// New accounts created after this feature will have this set to true.
/// </summary>
public bool IsCalendarAccessGranted { get; set; }
/// <summary>
/// Contains the merged inbox this account belongs to.
/// Ignored for all SQLite operations.
/// </summary>
[Ignore]
public MergedInbox MergedInbox { get; set; }
/// <summary>
/// Populated only when account has custom server information.
/// </summary>
[Ignore]
public CustomServerInformation ServerInformation { get; set; }
/// <summary>
/// Account preferences.
/// </summary>
[Ignore]
public MailAccountPreferences Preferences { get; set; }
/// <summary>
/// Last time folder structure was synchronized.
/// Used for optimization - skip folder sync if synced recently.
/// </summary>
public DateTime? LastFolderStructureSyncDate { get; set; }
/// <summary>
/// Gets or sets when the account was created in Wino.
/// </summary>
public DateTime? CreatedAt { get; set; }
/// <summary>
/// Gets or sets the timespan used for the account's initial mail synchronization.
/// </summary>
public InitialSynchronizationRange InitialSynchronizationRange { get; set; } = InitialSynchronizationRange.SixMonths;
/// <summary>
/// Gets whether the account can perform ProfileInformation sync type.
/// </summary>
public bool IsProfileInfoSyncSupported => ProviderType == MailProviderType.Outlook || ProviderType == MailProviderType.Gmail;
/// <summary>
/// Gets whether the account can perform AliasInformation sync type.
/// </summary>
public bool IsAliasSyncSupported => ProviderType == MailProviderType.Gmail || ProviderType == MailProviderType.Outlook;
/// <summary>
/// Gets whether the account can perform category definition sync type.
/// </summary>
public bool IsCategorySyncSupported => ProviderType == MailProviderType.Outlook;
public override string ToString() => Name;
}
@@ -0,0 +1,53 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities.Shared;
public class MailAccountPreferences
{
[PrimaryKey]
public Guid Id { get; set; }
/// <summary>
/// Id of the account in MailAccount table.
/// </summary>
public Guid AccountId { get; set; }
/// <summary>
/// Gets or sets whether sent draft messages should be appended to the sent folder.
/// Some IMAP servers do this automatically, some don't.
/// It's disabled by default.
/// </summary>
public bool ShouldAppendMessagesToSentFolder { get; set; }
/// <summary>
/// Gets or sets whether the notifications are enabled for the account.
/// </summary>
public bool IsNotificationsEnabled { get; set; }
/// <summary>
/// Gets or sets whether the account has Focused inbox support.
/// Null if the account provider type doesn't support Focused inbox.
/// </summary>
public bool? IsFocusedInboxEnabled { get; set; }
/// <summary>
/// Gets or sets whether signature should be appended automatically.
/// </summary>
public bool IsSignatureEnabled { get; set; }
/// <summary>
/// Gets or sets whether this account's unread items should be included in taskbar badge.
/// </summary>
public bool IsTaskbarBadgeEnabled { get; set; } = true;
/// <summary>
/// Gets or sets signature for new messages. Null if signature is not needed.
/// </summary>
public Guid? SignatureIdForNewMessages { get; set; }
/// <summary>
/// Gets or sets signature for following messages. Null if signature is not needed.
/// </summary>
public Guid? SignatureIdForFollowingMessages { get; set; }
}
@@ -0,0 +1,14 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities.Shared;
public class Thumbnail
{
[PrimaryKey]
public string Domain { get; set; }
public string Gravatar { get; set; }
public string Favicon { get; set; }
public DateTime LastUpdated { get; set; }
}
@@ -0,0 +1,30 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities.Shared;
public class WinoAccount
{
[PrimaryKey]
public Guid Id { get; set; }
public string Email { get; set; } = string.Empty;
public string AccountStatus { get; set; } = string.Empty;
public bool HasPassword { get; set; }
public bool HasGoogleLogin { get; set; }
public bool HasFacebookLogin { get; set; }
public string AccessToken { get; set; } = string.Empty;
public DateTime AccessTokenExpiresAtUtc { get; set; }
public string RefreshToken { get; set; } = string.Empty;
public DateTime RefreshTokenExpiresAtUtc { get; set; }
public DateTime LastAuthenticatedUtc { get; set; }
}
@@ -1,12 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Wino.Core.Domain.Entities
{
public record SystemFolderConfiguration(MailItemFolder SentFolder,
MailItemFolder DraftFolder,
MailItemFolder ArchiveFolder,
MailItemFolder TrashFolder,
MailItemFolder JunkFolder);
}
@@ -1,30 +0,0 @@
using System;
using SQLite;
using Wino.Core.Domain.Models.Authentication;
namespace Wino.Core.Domain.Entities
{
public class TokenInformation : TokenInformationBase
{
[PrimaryKey]
public Guid Id { get; set; }
public Guid AccountId { get; set; }
/// <summary>
/// Unique object storage for authenticators if needed.
/// </summary>
public string UniqueId { get; set; }
public string Address { get; set; }
public void RefreshTokens(TokenInformationBase tokenInformationBase)
{
if (tokenInformationBase == null)
throw new ArgumentNullException(nameof(tokenInformationBase));
AccessToken = tokenInformationBase.AccessToken;
RefreshToken = tokenInformationBase.RefreshToken;
ExpiresAt = tokenInformationBase.ExpiresAt;
}
}
}
@@ -1,9 +1,8 @@
namespace Wino.Core.Domain.Enums
namespace Wino.Core.Domain.Enums;
public enum AccountAttentionReason
{
public enum AccountAttentionReason
{
None,
InvalidCredentials,
MissingSystemFolderConfiguration
}
None,
InvalidCredentials,
MissingSystemFolderConfiguration
}
@@ -0,0 +1,7 @@
namespace Wino.Core.Domain.Enums;
public enum AccountCacheResetReason
{
AccountRemoval,
ExpiredCache
}

Some files were not shown because too many files have changed in this diff Show More