diff --git a/.github/labels.yml b/.github/issues/labels.yml similarity index 71% rename from .github/labels.yml rename to .github/issues/labels.yml index 388191c..f4e64e4 100644 --- a/.github/labels.yml +++ b/.github/issues/labels.yml @@ -1,3 +1,9 @@ +- name: a11y + description: about accessibility + color: fef2c0 +- name: await test repository + description: waiting for a minimal test repository + color: 5319e7 - name: bug description: bug to fix color: fbca04 @@ -9,7 +15,7 @@ color: 00209a - name: critical description: critical failure to fix as soon as possible - color: d93f0b + color: b60205 - name: dependencies description: update/upgrade one or more dependency color: 0366d6 @@ -19,14 +25,20 @@ - name: documentation description: regarding the project documentation color: 27d19b +- name: duplicate + description: a similar issue already exists + color: ededed - name: feature description: feature request to implement color: 1d76db - name: help description: need help, any contribution are welcome color: 5319e7 +- name: source map + description: about source mapping + color: ebe4da - name: motion - description: motion request + description: about motion and animation color: 32deca - name: optimization description: optimization request @@ -34,9 +46,9 @@ - name: post-it description: reminder or todo color: fcf49c -- name: platform +- name: environment description: regarding the environment or target platform color: f1f1f1 - name: wontfix description: negligible issue or that will never be fixed - color: 444444 + color: 000000 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 952ce85..ff2c16a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ # Github workflow for Continuous Integration -name: CI +name: 🤖 continuous integration on: push: @@ -13,7 +13,7 @@ on: - dev jobs: - build: + build-debug: runs-on: windows-latest defaults: run: @@ -35,11 +35,48 @@ jobs: run: | msbuild.exe InboxNotifier.sln /p:configuration="Debug" /p:platform="Any CPU" /m + build-x86: + runs-on: windows-latest + needs: build-debug + defaults: + run: + working-directory: code + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + + - name: Setup NuGet + uses: NuGet/setup-nuget@v2 + + - name: Restore NuGet packages + run: nuget restore InboxNotifier.sln + - name: Build solution — Release 32 bits (x86) run: | msbuild.exe InboxNotifier.sln /p:configuration="Release x86" /p:platform="Any CPU" /m + build-x64: + runs-on: windows-latest + needs: build-x86 + defaults: + run: + working-directory: code + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + + - name: Setup NuGet + uses: NuGet/setup-nuget@v2 + + - name: Restore NuGet packages + run: nuget restore InboxNotifier.sln + - name: Build solution — Release 64 bits (x64) run: | msbuild.exe InboxNotifier.sln /p:configuration="Release x64" /p:platform="Any CPU" /m - diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml index e86ae02..0742b78 100644 --- a/.github/workflows/sync-labels.yml +++ b/.github/workflows/sync-labels.yml @@ -1,14 +1,18 @@ # Github workflow to automatically sync labels in a declarative way # https://github.com/micnncim/action-label-syncer -name: Sync labels +name: 🏷️ synchronize labels on: push: branches: - main paths: - - '.github/labels.yml' + - .github/issues/labels.yml + +permissions: + contents: write + issues: write jobs: labels: @@ -16,5 +20,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: micnncim/action-label-syncer@v1 + with: + manifest: .github/issues/labels.yml env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ github.token }} diff --git a/README.md b/README.md index 0dd3952..86d9fae 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Note that this version of the .NET Framework **evolve in time**: the application #### Setup installer The setup installer contains both `32 bits (x86)` and `64 bits (x64)` versions: this is the better way to install Windows application and allow you to **cleanly install/uninstall** the application with ease. -* :package: [Inbox Notifier 3.11.0](https://github.com/xavierfoucrier/inbox-notifier/releases/download/v3.11.0/Inbox.Notifier.3.11.0.exe) +* :package: [Inbox Notifier 3.15.0](https://github.com/xavierfoucrier/inbox-notifier/releases/download/v3.15.0/Inbox.Notifier.3.15.0.exe) > Note that you will need **administrator rights** to properly complete the installation. diff --git a/code/App.config b/code/App.config index 51414ad..8ad4fa8 100644 --- a/code/App.config +++ b/code/App.config @@ -89,10 +89,6 @@ - - - - diff --git a/code/Computer.cs b/code/Computer.cs index 902eca8..b6e88d7 100644 --- a/code/Computer.cs +++ b/code/Computer.cs @@ -88,7 +88,7 @@ public void BindSessionSwitch() { } // synchronize the inbox and renew the token - await UI.GmailService.Inbox.Sync(true, true); + await UI.GmailService.Inbox.Sync(token: true); // enable the timer properly UI.timer.Enabled = true; diff --git a/code/Core.cs b/code/Core.cs index 0c54878..6822feb 100644 --- a/code/Core.cs +++ b/code/Core.cs @@ -21,11 +21,11 @@ static Core() { // initialize the application version number, based on scheme Semantic Versioning - https://semver.org string[] ProductVersion = Application.ProductVersion.Split('.'); - string VersionMajor = ProductVersion[0]; - string VersionMinor = ProductVersion[1]; - string VersionPatch = ProductVersion[2]; + MajorVersion = int.Parse(ProductVersion[0]); + MinorVersion = int.Parse(ProductVersion[1]); + PatchVersion = int.Parse(ProductVersion[2]); - Version = $"v{VersionMajor}.{VersionMinor}.{VersionPatch}"; + Version = $"v{MajorVersion}.{MinorVersion}.{PatchVersion}"; } /// @@ -34,13 +34,15 @@ static Core() { public static void RestartApplication() { // start a new process - Process.Start(new ProcessStartInfo("cmd.exe", $"/C ping 127.0.0.1 -n 2 && \"{Application.ExecutablePath}\"") { + Process.Start(new ProcessStartInfo { + FileName = Application.ExecutablePath, + UseShellExecute = true, WindowStyle = ProcessWindowStyle.Hidden, CreateNoWindow = true }); - // exit the application - Application.Exit(); + // exit the environment + Environment.Exit(0); } /// @@ -71,6 +73,27 @@ public static string Version { get; } = ""; + /// + /// Major application version number + /// + public static int MajorVersion { + get; + } = 0; + + /// + /// Minor application version number + /// + public static int MinorVersion { + get; + } = 0; + + /// + /// Patch application version number + /// + public static int PatchVersion { + get; + } = 0; + #endregion } } \ No newline at end of file diff --git a/code/Gmail.cs b/code/Gmail.cs index 310459b..7d0c756 100644 --- a/code/Gmail.cs +++ b/code/Gmail.cs @@ -123,12 +123,13 @@ public async Task Authentication() { // enable the main timer UI.timer.Enabled = true; - // synchronize the user mailbox, after checking for update depending on the user settings, or by default after the asynchronous authentication + // check for update depending on the user settings if (Settings.Default.UpdateService && Update.IsPeriodSetToStartup()) { await UI.UpdateService.Check(!Settings.Default.UpdateDownload, true); - } else { - await Inbox.Sync(); } + + // synchronize the user mailbox + await Inbox.Sync(); } /// @@ -139,7 +140,7 @@ public async Task RefreshToken() { // refresh the token and update the token delivery date and time on the interface try { - if (Credential.Token.IsExpired(Credential.Flow.Clock)) { + if (Credential.Token.IsStale) { if (await Credential.RefreshTokenAsync(new CancellationToken())) { UI.labelTokenDelivery.Text = Credential.Token.IssuedUtc.ToLocalTime().ToString(); } @@ -188,21 +189,22 @@ private static async Task AuthorizationBroker() { using (FileStream stream = new FileStream($"{Path.GetDirectoryName(Application.ExecutablePath)}/client_secret.json", FileMode.Open, FileAccess.Read)) { // define a cancellation token source - CancellationTokenSource cancellation = new CancellationTokenSource(); - cancellation.CancelAfter(TimeSpan.FromSeconds(Settings.Default.OAUTH_TIMEOUT)); - - // wait for the user validation, only if the user has not already authorized the application - UserCredential credential = await GoogleWebAuthorizationBroker.AuthorizeAsync( - GoogleClientSecrets.FromStream(stream).Secrets, - new string[] { GmailService.Scope.GmailModify }, - "user", - cancellation.Token, - new FileDataStore(Core.ApplicationDataFolder, true), - new LocalServerCodeReceiver(Resources.oauth_message) - ); - - // return the user credential - return credential; + using (CancellationTokenSource cancellation = new CancellationTokenSource()) { + cancellation.CancelAfter(TimeSpan.FromSeconds(Settings.Default.OAUTH_TIMEOUT)); + + // wait for the user validation, only if the user has not already authorized the application + UserCredential credential = await GoogleWebAuthorizationBroker.AuthorizeAsync( + GoogleClientSecrets.FromStream(stream).Secrets, + new string[] { GmailService.Scope.GmailModify }, + "user", + cancellation.Token, + new FileDataStore(Core.ApplicationDataFolder, true), + new LocalServerCodeReceiver(Resources.oauth_message) + ); + + // return the user credential + return credential; + } } } diff --git a/code/Inbox.cs b/code/Inbox.cs index b45b74b..1204542 100644 --- a/code/Inbox.cs +++ b/code/Inbox.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Media; using System.Net; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -17,6 +16,14 @@ class Inbox { #region #attributes + /// + /// Synchronization action + /// + public enum SyncAction : uint { + Automatic = 0, + Manual = 1 + } + /// /// Main user resource /// @@ -47,9 +54,9 @@ public Inbox(ref Main form) { /// /// Asynchronous method used to synchronize the user inbox /// - /// Indicate if the synchronization come's from the timer tick or has been manually triggered + /// Indicate if the synchronization come's from the timer tick or has been manually triggered by the user /// Indicate if the Gmail token need to be refreshed - public async Task Sync(bool manual = true, bool token = false) { + public async Task Sync(SyncAction action = SyncAction.Manual, bool token = false) { // do a small ping on the update service await UI.UpdateService.Ping(); @@ -59,8 +66,8 @@ public async Task Sync(bool manual = true, bool token = false) { return; } - // temp variable - bool userAction = manual; + // store last user action + Action = action; // update the synchronization time Time = DateTime.Now; @@ -74,12 +81,12 @@ public async Task Sync(bool manual = true, bool token = false) { // reset reconnection count and prevent the application from displaying continuous warning icon when a timertick synchronization occurs after a reconnection attempt if (ReconnectionAttempts != 0) { - userAction = true; + Action = SyncAction.Manual; ReconnectionAttempts = 0; } - // disable the timeout when the user do a manual synchronization - if (userAction && UI.NotificationService.Paused) { + // resume notification service when the user do a manual synchronization + if (Action == SyncAction.Manual && UI.NotificationService.Paused) { await UI.NotificationService.Resume(); return; @@ -104,7 +111,7 @@ public async Task Sync(bool manual = true, bool token = false) { UI.menuItemSettings.Enabled = true; // display the sync icon, but only on manual synchronization - if (userAction) { + if (Action == SyncAction.Manual) { UI.notifyIcon.Icon = Resources.sync; UI.notifyIcon.Text = Translation.sync; } @@ -120,175 +127,177 @@ public async Task Sync(bool manual = true, bool token = false) { Box = await User.Labels.Get("me", "INBOX").ExecuteAsync(); // update the statistics - if (userAction) { + if (Action == SyncAction.Manual) { await UpdateStatistics().ConfigureAwait(false); } - // manage the spam notification - if (Settings.Default.SpamNotification) { + // synchronize inbox spams + if (await SyncSpams()) { + return; + } - // exit if a spam is already detected - if (!userAction && UI.NotificationService.Tag == "#spam") { - return; - } + // synchronize inbox threads + if (await SyncThreads()) { + return; + } + } catch (IOException exception) { - // get the "spam" label - Label spam = await User.Labels.Get("me", "SPAM").ExecuteAsync(); + // log the exception from mscorlib: sometimes the process can not access the token response file because it is used by another process + Core.Log($"IOException: {exception.Message}"); + } catch (TokenResponseException exception) { - // manage unread spams - if (spam.ThreadsUnread > 0) { + // restart if the application has no Gmail grant access anymore + if (exception.Error.Error == "invalid_grant") { + Core.RestartApplication(); + } + } catch (Exception exception) { - // play a sound on unread spams - if (Settings.Default.AudioNotification) { - SystemSounds.Exclamation.Play(); - } + // display a balloon tip in the systray + UI.notifyIcon.Icon = Resources.warning; + UI.notifyIcon.Text = Translation.syncError; + UI.NotificationService.Tip(Translation.error, Translation.syncErrorOccured, Notification.Type.Warning, 1500); - // display a balloon tip in the systray with the total of unread threads - UI.NotificationService.Tip($"{spam.ThreadsUnread} {(spam.ThreadsUnread > 1 ? Translation.unreadSpams : Translation.unreadSpam)}", Translation.newUnreadSpam, Notification.Type.Error); + // log the error + Core.Log($"Sync: {exception.Message}"); + } finally { + UI.notifyIcon.Text = $"{UI.notifyIcon.Text.Split('\n')[0]}\n{Translation.syncTime.Replace("{time}", Time.ToLongTimeString())}"; + } + } - // set the notification icon and text - UI.notifyIcon.Icon = Resources.spam; - UI.notifyIcon.Text = $"{spam.ThreadsUnread} {(spam.ThreadsUnread > 1 ? Translation.unreadSpams : Translation.unreadSpam)}"; + /// + /// Asynchronous method used to synchronize the inbox spams + /// + /// Whether or not unread spams are present in the inbox + private async Task SyncSpams() { + if (!Settings.Default.SpamNotification) { + return false; + } - // enable the mark as read menu item - UI.menuItemMarkAsRead.Text = $"{Translation.markAsRead} ({spam.ThreadsUnread})"; - UI.menuItemMarkAsRead.Enabled = true; + // get the "spam" label + Label spam = await User.Labels.Get("me", "SPAM").ExecuteAsync(); - // update the tag - UI.NotificationService.Tag = "#spam"; + // exit the sync if the number of unread spams is the same as before + if (Action == SyncAction.Automatic && (spam.ThreadsUnread == UnreadSpams) && UnreadSpams != 0) { + return true; + } - return; - } - } + // save the number of unread spams + UnreadSpams = spam.ThreadsUnread; - // exit the sync if the number of unread threads is the same as before - if (!userAction && (Box.ThreadsUnread == UnreadThreads)) { - return; - } + // manage unread spams + if (UnreadSpams > 0) { - // manage unread threads - if (Box.ThreadsUnread > 0) { + // display a balloon tip in the systray with the total of unread threads + UI.NotificationService.Tip($"{UnreadSpams} {(UnreadSpams > 1 ? Translation.unreadSpams : Translation.unreadSpam)}", Translation.newUnreadSpam, Notification.Type.Error); - // set the notification icon - UI.notifyIcon.Icon = Box.ThreadsUnread <= Settings.Default.UNSTACK_BOUNDARY ? Resources.mails : Resources.stack; + // set the notification icon and text + UI.notifyIcon.Icon = Resources.spam; + UI.notifyIcon.Text = $"{UnreadSpams} {(UnreadSpams > 1 ? Translation.unreadSpams : Translation.unreadSpam)}"; - // manage message notification - if (Settings.Default.MessageNotification) { + // enable the mark as read menu item + UI.menuItemMarkAsRead.Text = $"{Translation.markAsRead} ({UnreadSpams})"; + UI.menuItemMarkAsRead.Enabled = true; - // play a sound on unread threads - if (Settings.Default.AudioNotification) { + // update the tag + UI.NotificationService.Tag = "#spam"; - // play a ringtone based on user setting - if (Settings.Default.Ringtone) { + return true; + } - // switch to the default ringtone if the audio file can't be found - if (File.Exists(Settings.Default.RingtoneFile)) { - using (SoundPlayer player = new SoundPlayer(Settings.Default.RingtoneFile)) { - player.Play(); - } - } else { - Settings.Default.Ringtone = false; - } - } else { - SystemSounds.Asterisk.Play(); - } - } + return false; + } - // get the message details - UsersResource.MessagesResource.ListRequest messages = User.Messages.List("me"); - messages.LabelIds = "UNREAD"; - messages.MaxResults = 1; - - Message message = await User.Messages.Get("me", await messages.ExecuteAsync().ContinueWith(m => { - return m.Result.Messages.First().Id; - })).ExecuteAsync(); - - // display a balloon tip in the systray with the total of unread threads and message details, depending on the user privacy setting - if (Box.ThreadsUnread == 1 && Settings.Default.PrivacyNotification != (uint)Notification.Privacy.All) { - string subject = ""; - string from = ""; - - foreach (MessagePartHeader header in message.Payload.Headers) { - string name = header.Name.ToLower(); - string value = header.Value; - - if (name == "subject") { - subject = string.IsNullOrEmpty(value) ? Translation.newUnreadMessage : value; - } else if (name == "from") { - Match match = Regex.Match(value, ".* <"); - - if (match.Length != 0) { - from = match.Captures[0].Value.Replace(" <", "").Replace("\"", ""); - } else { - match = Regex.Match(value, "?"); - from = match.Length != 0 ? match.Value.ToLower().Replace("<", "").Replace(">", "") : value.Replace(match.Value, $"{Box.ThreadsUnread} {Translation.unreadMessage}"); - } - } - } + /// + /// Asynchronous method used to synchronize the inbox threads + /// + /// Whether or not unread threads are present in the inbox + private async Task SyncThreads() { - if (Settings.Default.PrivacyNotification == (uint)Notification.Privacy.None) { - subject = string.IsNullOrEmpty(message.Snippet) ? Translation.newUnreadMessage : WebUtility.HtmlDecode(message.Snippet); - } + // exit the sync if the number of unread threads is the same as before + if (Action == SyncAction.Automatic && (Box.ThreadsUnread == UnreadThreads) && UnreadThreads != 0) { + return false; + } - // detect if the message contains attachments - if (message.Payload.Parts != null && message.Payload.MimeType == "multipart/mixed") { - int attachments = message.Payload.Parts.Where(part => !string.IsNullOrEmpty(part.Filename)).Count(); + // reset notification service + UI.NotificationService.Reset(); - if (attachments > 0) { - from = $"{(from.Length > 48 ? from.Substring(0, 48) : from)} - {attachments} {(attachments > 1 ? Translation.attachments : Translation.attachment)}"; - } - } + // save the number of unread threads + UnreadThreads = Box.ThreadsUnread; - UI.NotificationService.Tip(from, subject); - } else { - UI.NotificationService.Tip($"{Box.ThreadsUnread} {(Box.ThreadsUnread > 1 ? Translation.unreadMessages : Translation.unreadMessage)}", Translation.newUnreadMessage); - } + // manage unread threads + if (UnreadThreads > 0) { - // update the notification tag to allow the user to directly display the specified view (inbox/message/spam) in a browser - UI.NotificationService.Tag = $"#inbox{(Box.ThreadsUnread == 1 ? $"/{message.Id}" : "")}"; - } + // set the notification icon + UI.notifyIcon.Icon = UnreadThreads <= Settings.Default.UNSTACK_BOUNDARY ? Resources.mails : Resources.stack; + + // manage message notification + if (Settings.Default.MessageNotification) { - // display the notification text - UI.notifyIcon.Text = $"{Box.ThreadsUnread} {(Box.ThreadsUnread > 1 ? Translation.unreadMessages : Translation.unreadMessage)}"; + // get the message details + UsersResource.MessagesResource.ListRequest messages = User.Messages.List("me"); + messages.LabelIds = "UNREAD"; + messages.MaxResults = 1; - // enable the mark as read menu item - UI.menuItemMarkAsRead.Text = $"{Translation.markAsRead} ({Box.ThreadsUnread})"; - UI.menuItemMarkAsRead.Enabled = true; - } else { + Message message = await User.Messages.Get("me", await messages.ExecuteAsync().ContinueWith(m => { + return m.Result.Messages.First().Id; + })).ExecuteAsync(); - // restore the default systray icon and text - UI.notifyIcon.Icon = Resources.normal; - UI.notifyIcon.Text = Translation.noMessage; + // display a balloon tip in the systray with the total of unread threads and message details, depending on the user privacy setting + if (UnreadThreads == 1 && Settings.Default.PrivacyNotification != (uint)Notification.Privacy.All) { + string subject = ""; + string from = ""; - // disable the mark as read menu item - UI.menuItemMarkAsRead.Text = Translation.markAsRead; - UI.menuItemMarkAsRead.Enabled = false; - } + foreach (MessagePartHeader header in message.Payload.Headers) { + string name = header.Name.ToLower(); + string value = header.Value; - // save the number of unread threads - UnreadThreads = Box.ThreadsUnread; - } catch (IOException exception) { + if (name == "subject") { + subject = string.IsNullOrEmpty(value) ? Translation.newUnreadMessage : value; + } else if (name == "from") { + Match match = Regex.Match(value, ".* <"); - // log the exception from mscorlib: sometimes the process can not access the token response file because it is used by another process - Core.Log($"IOException: {exception.Message}"); - } catch (TokenResponseException exception) { + if (match.Length != 0) { + from = match.Captures[0].Value.Replace(" <", "").Replace("\"", ""); + } else { + match = Regex.Match(value, "?"); + from = match.Length != 0 ? match.Value.ToLower().Replace("<", "").Replace(">", "") : value.Replace(match.Value, $"{UnreadThreads} {Translation.unreadMessage}"); + } + } + } - // restart if the application has no Gmail grant access anymore - if (exception.Error.Error == "invalid_grant") { - Core.RestartApplication(); + if (Settings.Default.PrivacyNotification == (uint)Notification.Privacy.None) { + subject = string.IsNullOrEmpty(message.Snippet) ? Translation.newUnreadMessage : WebUtility.HtmlDecode(message.Snippet); + } + + // detect if the message contains attachments + if (message.Payload.Parts != null && message.Payload.MimeType == "multipart/mixed") { + int attachments = message.Payload.Parts.Where(part => !string.IsNullOrEmpty(part.Filename)).Count(); + + if (attachments > 0) { + from = $"{(from.Length > 48 ? from.Substring(0, 48) : from)} - {attachments} {(attachments > 1 ? Translation.attachments : Translation.attachment)}"; + } + } + + UI.NotificationService.Tip(from, subject); + } else { + UI.NotificationService.Tip($"{UnreadThreads} {(UnreadThreads > 1 ? Translation.unreadMessages : Translation.unreadMessage)}", Translation.newUnreadMessage); + } + + // update the notification tag to allow the user to directly display the specified view (inbox/message/spam) in a browser + UI.NotificationService.Tag = $"#inbox{(UnreadThreads == 1 ? $"/{message.Id}" : "")}"; } - } catch (Exception exception) { - // display a balloon tip in the systray - UI.notifyIcon.Icon = Resources.warning; - UI.notifyIcon.Text = Translation.syncError; - UI.NotificationService.Tip(Translation.error, Translation.syncErrorOccured, Notification.Type.Warning, 1500); + // display the notification text + UI.notifyIcon.Text = $"{UnreadThreads} {(UnreadThreads > 1 ? Translation.unreadMessages : Translation.unreadMessage)}"; - // log the error - Core.Log($"Sync: {exception.Message}"); - } finally { - UI.notifyIcon.Text = $"{UI.notifyIcon.Text.Split('\n')[0]}\n{Translation.syncTime.Replace("{time}", Time.ToLongTimeString())}"; + // enable the mark as read menu item + UI.menuItemMarkAsRead.Text = $"{Translation.markAsRead} ({UnreadThreads})"; + UI.menuItemMarkAsRead.Enabled = true; + + return true; } + + return false; } /// @@ -310,9 +319,9 @@ public async Task MarkAsRead() { }; // check for unread spams - bool unreadSpams = UI.NotificationService.Tag == "#spam"; + bool spams = UnreadSpams > 0; - if (unreadSpams) { + if (spams) { filter.Add("SPAM"); } @@ -343,29 +352,16 @@ public async Task MarkAsRead() { Box = await User.Labels.Get("me", "INBOX").ExecuteAsync(); // update the statistics only when there is no unread spams - if (!unreadSpams) { + if (!spams) { await UpdateStatistics().ConfigureAwait(false); } } // sync the inbox again if the user has just mark spams as read - if (unreadSpams) { + if (spams) { await Sync().ConfigureAwait(false); } else { - - // restore the default systray icon and text - UI.notifyIcon.Icon = Resources.normal; - UI.notifyIcon.Text = Translation.noMessage; - - // clean the tag - UI.NotificationService.Tag = null; - - // reset the number of unread threads - UnreadThreads = 0; - - // disable the mark as read menu item - UI.menuItemMarkAsRead.Text = Translation.markAsRead; - UI.menuItemMarkAsRead.Enabled = false; + UI.NotificationService.Reset(); } } catch (TokenResponseException exception) { @@ -509,6 +505,13 @@ public async Task UpdateStatistics() { #region #accessors + /// + /// Last synchronization action + /// + public SyncAction Action { + get; set; + } = SyncAction.Automatic; + /// /// Last synchronization time /// @@ -523,6 +526,13 @@ public int? UnreadThreads { get; set; } = 0; + /// + /// Unread spams + /// + public int? UnreadSpams { + get; set; + } = 0; + /// /// Number of automatic reconnection attempts /// diff --git a/code/InboxNotifier.csproj b/code/InboxNotifier.csproj index ff74fa9..ce5249f 100644 --- a/code/InboxNotifier.csproj +++ b/code/InboxNotifier.csproj @@ -227,13 +227,7 @@ - 1.67.0.3287 - - - 1.11.60 - - - 2.0.17 + 1.68.0.3427 13.0.3 diff --git a/code/Languages/Translation.Designer.cs b/code/Languages/Translation.Designer.cs index 2649caf..5d05037 100644 --- a/code/Languages/Translation.Designer.cs +++ b/code/Languages/Translation.Designer.cs @@ -19,7 +19,7 @@ namespace notifier.Languages { // à l'aide d'un outil, tel que ResGen ou Visual Studio. // Pour ajouter ou supprimer un membre, modifiez votre fichier .ResX, puis réexécutez ResGen // avec l'option /str ou régénérez votre projet VS. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Translation { @@ -531,6 +531,15 @@ internal static string mutexError { } } + /// + /// Recherche une chaîne localisée semblable à Une nouvelle version majeure de l'application est disponible sur Github : {version}. + /// + internal static string newMajorVersion { + get { + return ResourceManager.GetString("newMajorVersion", resourceCulture); + } + } + /// /// Recherche une chaîne localisée semblable à Double-cliquez sur l'icône pour accéder à votre boîte de réception.. /// diff --git a/code/Languages/Translation.de.resx b/code/Languages/Translation.de.resx index 0630629..fd34a86 100644 --- a/code/Languages/Translation.de.resx +++ b/code/Languages/Translation.de.resx @@ -282,6 +282,9 @@ Möchten Sie ihr Gmail Konto wirklich abmelden und die Anwendung neu starten? + + Eine neuere Hauptversion der Anwendung auf Github ist verfügbar: {version} + Doppelklicken Sie auf das Symbol, um auf Ihren Posteingang zuzugreifen. diff --git a/code/Languages/Translation.en.resx b/code/Languages/Translation.en.resx index e596038..858883b 100644 --- a/code/Languages/Translation.en.resx +++ b/code/Languages/Translation.en.resx @@ -282,6 +282,9 @@ Do you really want to disconnect your Gmail account and restart the application This option is not enabled on this type of application. + + A newer major version of the application is available on Github: {version} + Double-click the icon to access your inbox. diff --git a/code/Languages/Translation.resx b/code/Languages/Translation.resx index 3d07699..587f647 100644 --- a/code/Languages/Translation.resx +++ b/code/Languages/Translation.resx @@ -282,6 +282,9 @@ Voulez-vous vraiment déconnecter votre compte Google et redémarrer l'applicati Cette option n'est pas activée sur ce type d'application. + + Une nouvelle version majeure de l'application est disponible sur Github : {version} + Double-cliquez sur l'icône pour accéder à votre boîte de réception. diff --git a/code/Languages/Translation.ru.resx b/code/Languages/Translation.ru.resx index 2bf2f52..76adf74 100644 --- a/code/Languages/Translation.ru.resx +++ b/code/Languages/Translation.ru.resx @@ -282,6 +282,9 @@ Эта опция не включена для данного типа приложения. + + Более новая версия приложения доступна на Github: {version} + Дважды щёлкните по значку для доступа к папке "Входящие". diff --git a/code/Main.cs b/code/Main.cs index 7b59209..292c7f7 100644 --- a/code/Main.cs +++ b/code/Main.cs @@ -533,7 +533,7 @@ private async void timer_Tick(object sender, EventArgs e) { } // synchronize the inbox - await GmailService.Inbox.Sync(false); + await GmailService.Inbox.Sync(Inbox.SyncAction.Automatic); } /// @@ -588,7 +588,12 @@ private async void buttonCheckForUpdate_Click(object sender, EventArgs e) { WindowState = FormWindowState.Minimized; ShowInTaskbar = false; Visible = false; - await UpdateService.Download(); + + if (UpdateService.MajorUpdateAvailable) { + UpdateService.ShowGithubRelease(); + } else { + await UpdateService.Download(); + } } else { await UpdateService.Check(); } diff --git a/code/Main.de.resx b/code/Main.de.resx index c330624..1739a54 100644 --- a/code/Main.de.resx +++ b/code/Main.de.resx @@ -529,13 +529,13 @@ als gelesen markiert. Open Source, veröffentlicht unter der MIT-Lizenz - 41, 15 + 42, 15 153, 34 - Visual Studio 17.8.3, C# + Visual Studio 17.12.2, C# Entwickelt von Xavier Foucrier diff --git a/code/Main.en.resx b/code/Main.en.resx index 853f3e4..a83de72 100644 --- a/code/Main.en.resx +++ b/code/Main.en.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + General + 194, 108 @@ -143,6 +146,9 @@ at the next application startup. Language: + + Behavior + 157, 17 @@ -167,11 +173,11 @@ at the next application startup. Ask before leaving the application - - Behavior + + Account - - General + + Statistics 38, 13 @@ -185,8 +191,8 @@ at the next application startup. Inbox - - Statistics + + Authentication 83, 13 @@ -203,11 +209,8 @@ at the next application startup. Sign out - - Authentication - - - Account + + Behavior do nothing @@ -261,8 +264,8 @@ will mark all inbox messages as read. Upon receipt of multiple messages, this option will automatically open the inbox. - - Behavior + + Mail @@ -309,8 +312,8 @@ will automatically open the inbox. Sound notification - - Mail + + Privacy 172, 13 @@ -336,8 +339,11 @@ will automatically open the inbox. Display all message content - - Privacy + + Schedule + + + Time slot 47, 13 @@ -399,8 +405,8 @@ will automatically open the inbox. sunday - - Time slot + + Behavior 195, 17 @@ -420,11 +426,11 @@ will automatically open the inbox. Enable scheduled synchronization - - Behavior + + Updates - - Schedule + + Availability 165, 99 @@ -459,8 +465,8 @@ will automatically open the inbox. Last checked: - - Availability + + Behavior 13, 84 @@ -496,11 +502,8 @@ will automatically open the inbox. Enable automatic update checking - - Behavior - - - Updates + + About @@ -523,13 +526,13 @@ will automatically open the inbox. Open Source, distributed under MIT License - 39, 15 + 40, 15 150, 34 - Visual Studio 17.8.3, C# + Visual Studio 17.12.2, C# Developed by Xavier Foucrier @@ -551,9 +554,6 @@ a Tokyo-based designer and developer Development - - About - 223, 13 @@ -596,6 +596,9 @@ a Tokyo-based designer and developer Mark as read + + Do not disturb + Disable @@ -611,9 +614,6 @@ a Tokyo-based designer and developer Indefinitely - - Do not disturb - Open diff --git a/code/Main.resx b/code/Main.resx index 95acbf1..3db90c1 100644 --- a/code/Main.resx +++ b/code/Main.resx @@ -1142,7 +1142,7 @@ de réception comme lus. linkVersion - Visual Studio 17.8.3, C# + Visual Studio 17.12.2, C# Développé par Xavier Foucrier @@ -1864,7 +1864,7 @@ prochain démarrage de l'application. 2, 3, 2, 3 - 40, 15 + 41, 15 NoControl diff --git a/code/Main.ru.resx b/code/Main.ru.resx index 4ccae10..b8560f4 100644 --- a/code/Main.ru.resx +++ b/code/Main.ru.resx @@ -117,6 +117,12 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Общее + + + Интерфейс + 178, 109 @@ -143,8 +149,8 @@ Язык: - - Интерфейс + + Поведение 167, 17 @@ -170,11 +176,11 @@ Спрашивать при выходе из приложения - - Поведение + + Аккаунт - - Общее + + Статистика 67, 13 @@ -194,8 +200,8 @@ Входящие - - Статистика + + Проверка подлинности 99, 13 @@ -215,11 +221,11 @@ Выйти - - Проверка подлинности + + Уведомление - - Аккаунт + + Поведение ничего не делать @@ -274,8 +280,8 @@ При получении нескольких сообщений эта опция автоматически откроет папку "Входящие". - - Поведение + + Почта @@ -325,10 +331,10 @@ Звуковое уведомление - - Почта + + Конфиденциальность - + Уведомление @@ -355,11 +361,11 @@ Показывать всё содержимое сообщения - - Уведомление + + Расписание - - Конфиденциальность + + Временной интервал 70, 13 @@ -421,8 +427,8 @@ воскресенье - - Временной интервал + + Поведение @@ -464,11 +470,11 @@ Включить синхронизацию по расписанию - - Поведение + + Обновления - - Расписание + + Доступность 172, 99 @@ -503,8 +509,8 @@ Последняя проверка: - - Доступность + + Поведение 112, 17 @@ -524,11 +530,8 @@ Включить автоматическую проверку обновлений - - Поведение - - - Обновления + + Инфо @@ -551,13 +554,13 @@ Открытый исходный код, лицензия MIT - 38, 15 + 39, 15 148, 34 - Visual Studio 17.8.3, C# + Visual Studio 17.12.2, C# Разработано Xavier Foucrier @@ -591,9 +594,6 @@ Версия - - Инфо - 237, 13 @@ -633,6 +633,9 @@ Отметить как прочитанное + + Не беспокоить + Отключить @@ -651,9 +654,6 @@ Бессрочно - - Не беспокоить - Открыть diff --git a/code/Notification.cs b/code/Notification.cs index 9c2a34b..a03fa6d 100644 --- a/code/Notification.cs +++ b/code/Notification.cs @@ -1,5 +1,7 @@ using System; using System.Diagnostics; +using System.IO; +using System.Media; using System.Threading.Tasks; using System.Windows.Forms; using notifier.Languages; @@ -65,6 +67,9 @@ public Notification(ref Main form) { /// How long the notification is displayed public void Tip(string title, string text, Type icon = Type.Info, int duration = 450) { UI.notifyIcon.ShowBalloonTip(duration, title, text, (ToolTipIcon)icon); + + // play audio based on balloon type + Ringtone(icon); } /// @@ -73,28 +78,29 @@ public void Tip(string title, string text, Type icon = Type.Info, int duration = /// Define if the interaction is provided by the balloon tip public async Task Interaction(bool balloon = false) { - // by default, always open the gmail inbox in a browser if the interaction is provided by a double click on the systray icon - if (Tag == null) { - - if (!balloon) { - Process.Start($"{GetBaseURL()}/#inbox"); - } - - return; - } - // display the form and focus the update tab - if (balloon && Tag == "update") { + if (balloon && UI.UpdateService.UpdateAvailable && (Tag == "update" || Tag == null)) { UI.Visible = true; UI.ShowInTaskbar = true; UI.WindowState = FormWindowState.Normal; UI.Focus(); UI.tabControl.SelectTab("tabPageUpdate"); + UI.buttonCheckForUpdate.Focus(); Tag = null; return; } + // by default, always open the gmail inbox in a browser if the interaction is provided by a double click on the systray icon + if (Tag == null) { + + if (!balloon) { + Process.Start($"{GetBaseURL()}/#inbox"); + } + + return; + } + // do nothing if the notification behavior is set to "do nothing" if (balloon && Settings.Default.NotificationBehavior == (uint)Behavior.DoNothing) { return; @@ -221,6 +227,58 @@ public async Task Resume() { await UI.GmailService.Inbox.Sync(); } + /// + /// Reset the notification area + /// + public void Reset() { + + // restore the default systray icon and text + UI.notifyIcon.Icon = Resources.normal; + UI.notifyIcon.Text = Translation.noMessage; + + // clean the tag + UI.NotificationService.Tag = null; + + // disable the mark as read menu item + UI.menuItemMarkAsRead.Text = Translation.markAsRead; + UI.menuItemMarkAsRead.Enabled = false; + } + + /// + /// Play ringtone based on displayed notification + /// + /// Type of the ringtone + private void Ringtone(Type type = Type.Info) { + if (!Settings.Default.AudioNotification) { + return; + } + + // play audio based on type + switch (type) { + case Type.Info: + + // play a ringtone based on user setting + if (Settings.Default.Ringtone) { + + // switch to the default ringtone if the audio file can't be found + if (File.Exists(Settings.Default.RingtoneFile)) { + using (SoundPlayer player = new SoundPlayer(Settings.Default.RingtoneFile)) { + player.Play(); + } + } else { + Settings.Default.Ringtone = false; + } + } else { + SystemSounds.Asterisk.Play(); + } + + break; + case Type.Error: + SystemSounds.Exclamation.Play(); + break; + } + } + /// /// Return the Gmail base URL depending on the notification behavior /// diff --git a/code/Properties/AssemblyInfo.cs b/code/Properties/AssemblyInfo.cs index d634213..d00db16 100644 --- a/code/Properties/AssemblyInfo.cs +++ b/code/Properties/AssemblyInfo.cs @@ -37,6 +37,6 @@ // Vous pouvez spécifier toutes les valeurs ou indiquer les numéros de build et de révision par défaut // en utilisant '*', comme indiqué ci-dessous : // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("3.11.0.0")] -[assembly: AssemblyFileVersion("3.11.0.0")] +[assembly: AssemblyVersion("3.15.0.0")] +[assembly: AssemblyFileVersion("3.15.0.0")] [assembly: NeutralResourcesLanguageAttribute("fr-FR")] \ No newline at end of file diff --git a/code/Resources/oauth/message.html b/code/Resources/oauth/message.html index 9845d0c..8ecb903 100644 --- a/code/Resources/oauth/message.html +++ b/code/Resources/oauth/message.html @@ -24,8 +24,8 @@ height: 100vh; margin: 0; background: var(--contrast-color) linear-gradient(to bottom right, var(--contrast-color), var(--darken-color)); - font-family: 'Inter UI', Arial, sans-serif; - font-size: 15px; + font-family: var(--font); + font-size: 1.2em; color: var(--base-color); text-align: left; user-select: none; @@ -44,12 +44,13 @@ } small { - font-size: 10px; + font-size: 12px; opacity: 0.5; } .box { - max-width: 500px; + max-width: 550px; + padding: 50px; } .message { diff --git a/code/Update.cs b/code/Update.cs index 5ccaec9..22e217f 100644 --- a/code/Update.cs +++ b/code/Update.cs @@ -3,11 +3,14 @@ using System.ComponentModel; using System.Diagnostics; using System.IO; +using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows.Forms; -using HtmlAgilityPack; +using Newtonsoft.Json.Linq; using notifier.Languages; using notifier.Properties; @@ -31,6 +34,11 @@ private enum Period : uint { /// private readonly HttpClient Http = new HttpClient(); + /// + /// Update endpoint + /// + private readonly string EndPoint = "https://api.github.com/repos/xavierfoucrier/inbox-notifier/releases"; + /// /// Reference to the main interface /// @@ -114,28 +122,18 @@ public async Task Ping() { public async Task Check(bool verbose = true, bool startup = false) { try { - // using tls 1.2 as security protocol to contact Github.com - ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; - - // get the latest tag in the Github repository tags webpage - HttpResponseMessage response = await Http.GetAsync($"{Settings.Default.GITHUB_REPOSITORY}/tags"); - - HtmlAgilityPack.HtmlDocument document = new HtmlAgilityPack.HtmlDocument(); - document.LoadHtml(await response.Content.ReadAsStringAsync()); - - HtmlNode node = document.DocumentNode.SelectSingleNode("//a[contains(@href, 'releases/tag/v')]"); - - if (node == null) { + // define user agent + Http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("InboxNotifier", Core.Version)); - // indicate to the user that the update service is not reachable for the moment - if (verbose) { - UI.NotificationService.Tip(Translation.updateServiceName, Translation.updateServiceUnreachable, Notification.Type.Warning, 1500); - } + // request the open Github API + HttpResponseMessage httpResponse = await Http.GetAsync(EndPoint); + httpResponse.EnsureSuccessStatusCode(); - return; - } + string responseBody = await httpResponse.Content.ReadAsStringAsync(); + JArray releases = JArray.Parse(responseBody); - string release = node.InnerText.Trim(); + // filter by releases only (exclude pre-releases) + string release = releases.Where(version => !(bool)version["prerelease"]).First()["tag_name"].ToString(); // store the latest update datetime control Settings.Default.UpdateControl = DateTime.Now; @@ -156,11 +154,26 @@ public async Task Check(bool verbose = true, bool startup = false) { // update the check for update button text UI.buttonCheckForUpdate.Text = Translation.updateNow; - // download the update package automatically or ask the user, depending on the user setting and verbosity - if (verbose) { - UI.NotificationService.Tip(Translation.updateServiceName, Translation.newVersion.Replace("{version}", ReleaseAvailable), Notification.Type.Info, 1500); - } else if (Settings.Default.UpdateDownload) { - await Download().ConfigureAwait(false); + // check for major version changes + int major = int.Parse(Regex.Match(release, @"v(\d+)").Groups[1].Value); + + if (major > Core.MajorVersion) { + + // store the major update state + MajorUpdateAvailable = true; + + // notify the user about new major release + if (verbose) { + UI.NotificationService.Tip(Translation.updateServiceName, Translation.newMajorVersion.Replace("{version}", ReleaseAvailable), Notification.Type.Info, 1500); + } + } else { + + // download the update package automatically or ask the user, depending on the user setting and verbosity + if (verbose) { + UI.NotificationService.Tip(Translation.updateServiceName, Translation.newVersion.Replace("{version}", ReleaseAvailable), Notification.Type.Info, 1500); + } else if (Settings.Default.UpdateDownload) { + await Download().ConfigureAwait(false); + } } } else if (verbose && !startup) { MessageBox.Show(Translation.latestVersion, Translation.updateServiceName, MessageBoxButtons.OK, MessageBoxIcon.Information); @@ -178,11 +191,6 @@ public async Task Check(bool verbose = true, bool startup = false) { // restore default update button state UI.buttonCheckForUpdate.Enabled = true; - - // synchronize the inbox if the updates has been checked at startup after asynchronous authentication - if (startup) { - await UI.GmailService.Inbox.Sync(); - } } } @@ -218,12 +226,18 @@ public async Task Download() { // start the setup installer when the download has complete and exit the current application client.DownloadFileCompleted += (object source, AsyncCompletedEventArgs target) => { - Process.Start(new ProcessStartInfo("cmd.exe", $"/C ping 127.0.0.1 -n 2 && \"{updatepath}\" {(Settings.Default.UpdateQuiet ? "/verysilent" : "")}") { + + // start a new process + Process.Start(new ProcessStartInfo { + FileName = updatepath, + UseShellExecute = true, WindowStyle = ProcessWindowStyle.Hidden, - CreateNoWindow = true + CreateNoWindow = true, + Arguments = Settings.Default.UpdateQuiet ? "/verysilent" : "" }); - Application.Exit(); + // exit the environment + Environment.Exit(0); }; // ensure that the Github package URI is callable @@ -251,6 +265,18 @@ public async Task Download() { } } + /// + /// Show the Github release page in a browser + /// + public void ShowGithubRelease() { + + // open the release link in a browser + Process.Start($"{Settings.Default.GITHUB_REPOSITORY}/releases/tag/{ReleaseAvailable}"); + + // restore default update button state + UI.buttonCheckForUpdate.Enabled = true; + } + #endregion #region #accessors @@ -260,7 +286,14 @@ public async Task Download() { /// public bool UpdateAvailable { get; set; - } + } = false; + + /// + /// Flag defining if a major update is available + /// + public bool MajorUpdateAvailable { + get; set; + } = false; /// /// Latest release version available @@ -274,7 +307,7 @@ public string ReleaseAvailable { /// public bool Updating { get; set; - } + } = false; #endregion } diff --git a/setup/setup.iss b/setup/setup.iss index f8c55c5..6a3b654 100644 --- a/setup/setup.iss +++ b/setup/setup.iss @@ -1,5 +1,5 @@ #define MyAppName "Inbox Notifier" -#define MyAppVersion "3.11.0" +#define MyAppVersion "3.15.0" #define MyAppYear GetDateTimeString('yyyy', '', ''); #define MyAppPublisher "Xavier Foucrier" #define MyAppURL "https://github.com/xavierfoucrier/inbox-notifier"