From ad45cf8c721215ef1a55958c6eb2492195e763e7 Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Tue, 10 Apr 2018 21:18:08 +0200
Subject: [PATCH] Begin rewriting update checker to run within C#

---
 Core/FormBrowser.cs                       |  36 ++++--
 Core/Other/Settings/TabSettingsGeneral.cs |  10 +-
 Resources/Scripts/update.js               |  78 +-----------
 TweetDuck.csproj                          |   2 +
 Updates/Events/UpdateCheckEventArgs.cs    |   7 +-
 Updates/FormUpdateDownload.cs             |  15 +--
 Updates/UpdateCheckClient.cs              |  63 ++++++++++
 Updates/UpdateDownloadStatus.cs           |   7 ++
 Updates/UpdateHandler.cs                  | 140 +++++++++++++++-------
 Updates/UpdateInfo.cs                     |  39 +++---
 10 files changed, 238 insertions(+), 159 deletions(-)
 create mode 100644 Updates/UpdateCheckClient.cs

diff --git a/Core/FormBrowser.cs b/Core/FormBrowser.cs
index 26a009f7..a895c02b 100644
--- a/Core/FormBrowser.cs
+++ b/Core/FormBrowser.cs
@@ -78,8 +78,9 @@ public FormBrowser(UpdaterSettings updaterSettings){
             Disposed += (sender, args) => {
                 Config.MuteToggled -= Config_MuteToggled;
                 Config.TrayBehaviorChanged -= Config_TrayBehaviorChanged;
-
+                
                 browser.Dispose();
+                updates.Dispose();
                 contextMenu.Dispose();
 
                 notificationScreenshotManager?.Dispose();
@@ -96,6 +97,7 @@ public FormBrowser(UpdaterSettings updaterSettings){
             UpdateTrayIcon();
 
             this.updates = new UpdateHandler(browser, updaterSettings);
+            this.updates.CheckFinished += updates_CheckFinished;
             this.updates.UpdateAccepted += updates_UpdateAccepted;
             this.updates.UpdateDismissed += updates_UpdateDismissed;
 
@@ -233,6 +235,18 @@ private static void plugins_Executed(object sender, PluginErrorEventArgs e){
             }
         }
 
+        private void updates_CheckFinished(object sender, UpdateCheckEventArgs e){
+            this.InvokeAsyncSafe(() => {
+                e.Result.Handle(update => {
+                    if (!update.IsUpdateNew && !update.IsUpdateDismissed){
+                        updates.StartTimer();
+                    }
+                }, ex => {
+                    // TODO show error and ask if the user wants to temporarily disable checks -- or maybe only do that for the first check of the day
+                });
+            });
+        }
+
         private void updates_UpdateAccepted(object sender, UpdateEventArgs e){
             this.InvokeAsyncSafe(() => {
                 FormManager.CloseAllDialogs();
@@ -241,13 +255,21 @@ private void updates_UpdateAccepted(object sender, UpdateEventArgs e){
                     Config.DismissedUpdate = null;
                     Config.Save();
                 }
-            
-                updates.BeginUpdateDownload(this, e.UpdateInfo, update => {
-                    if (update.DownloadStatus == UpdateDownloadStatus.Done){
-                        UpdateInstallerPath = update.InstallerPath;
-                    }
 
-                    ForceClose();
+                updates.BeginUpdateDownload(this, e.UpdateInfo, update => {
+                    UpdateDownloadStatus status = update.DownloadStatus;
+
+                    if (status == UpdateDownloadStatus.Done){
+                        UpdateInstallerPath = update.InstallerPath;
+                        ForceClose();
+                    }
+                    else if (FormMessage.Error("Update Has Failed", "Could not automatically download the update: "+(update.DownloadError?.Message ?? "unknown error")+"\n\nWould you like to open the website and try downloading the update manually?", FormMessage.Yes, FormMessage.No)){
+                        BrowserUtils.OpenExternalBrowser(Program.Website);
+                        ForceClose();
+                    }
+                    else{
+                        Show();
+                    }
                 });
             });
         }
diff --git a/Core/Other/Settings/TabSettingsGeneral.cs b/Core/Other/Settings/TabSettingsGeneral.cs
index 1d36ee8b..8dda7c37 100644
--- a/Core/Other/Settings/TabSettingsGeneral.cs
+++ b/Core/Other/Settings/TabSettingsGeneral.cs
@@ -222,9 +222,13 @@ private void updates_CheckFinished(object sender, UpdateCheckEventArgs e){
                 if (e.EventId == updateCheckEventId){
                     btnCheckUpdates.Enabled = true;
 
-                    if (!e.IsUpdateAvailable){
-                        FormMessage.Information("No Updates Available", "Your version of TweetDuck is up to date.", FormMessage.OK);
-                    }
+                    e.Result.Handle(update => {
+                        if (!update.IsUpdateNew){
+                            FormMessage.Information("No Updates Available", "Your version of TweetDuck is up to date.", FormMessage.OK);
+                        }
+                    }, ex => {
+                        Program.Reporter.HandleException("Update Check", "Encountered an error while checking updates.", true, ex);
+                    });
                 }
             });
         }
diff --git a/Resources/Scripts/update.js b/Resources/Scripts/update.js
index 6d3ffa98..08444ef4 100644
--- a/Resources/Scripts/update.js
+++ b/Resources/Scripts/update.js
@@ -1,33 +1,9 @@
 (function($, $TDU){
-  //
-  // Variable: Current timeout ID for update checking.
-  //
-  var updateCheckTimeoutID;
-  
-  //
-  // Constant: Update exe file name.
-  //
-  const updateFileName = "TweetDuck.Update.exe";
-  
-  //
-  // Constant: Url that returns JSON data about latest version.
-  //
-  const updateCheckUrlLatest = "https://api.github.com/repos/chylex/TweetDuck/releases/latest";
-  
-  //
-  // Constant: Url that returns JSON data about all versions, including prereleases.
-  //
-  const updateCheckUrlAll = "https://api.github.com/repos/chylex/TweetDuck/releases";
-  
-  //
-  // Constant: Fallback url in case the update installer file is missing.
-  //
-  const updateDownloadFallback = "https://tweetduck.chylex.com";
-  
   //
   // Function: Creates the update notification element. Removes the old one if already exists.
   //
-  var displayNotification = function(version, download, changelog){
+  var displayNotification = function(version, changelog){
+    
     // styles
     var css = $("#tweetduck-update-css");
     
@@ -167,7 +143,7 @@
 <div id='tweetduck-changelog'>
   <div id='tweetduck-changelog-box'>
     <h2>TweetDuck Update ${version}</h2>
-    ${changelog}
+    ${markdown(atob(changelog))}
   </div>
 </div>
 `).appendTo(document.body).css("display", "none");
@@ -219,17 +195,11 @@
 
     buttonDiv.children(".tdu-btn-download").click(function(){
       hide();
-      
-      if (download){
-        $TDU.onUpdateAccepted();
-      }
-      else{
-        $TDU.openBrowser(updateDownloadFallback);
-      }
+      $TDU.onUpdateAccepted();
     });
     
     buttonDiv.children(".tdu-btn-later").click(function(){
-      clearTimeout(updateCheckTimeoutID);
+      $TDU.onUpdateDelayed();
       slide();
     });
 
@@ -245,15 +215,6 @@
     return ele;
   };
   
-  //
-  // Function: Returns milliseconds until the start of the next hour, with an extra offset in seconds that can skip an hour if the clock would roll over too soon.
-  //
-  var getTimeUntilNextHour = function(extra){
-    var now = new Date();
-    var offset = new Date(+now+extra*1000);
-    return new Date(offset.getFullYear(), offset.getMonth(), offset.getDate(), offset.getHours()+1, 0, 0)-now;
-  };
-  
   //
   // Function: Ghetto-converts markdown to HTML.
   //
@@ -273,33 +234,6 @@
              .replace(/\n\r?\n\r?/g, "<br>");
   };
   
-  //
-  // Function: Runs an update check and updates all DOM elements appropriately.
-  //
-  var runUpdateCheck = function(eventID, versionTag, dismissedVersionTag, allowPre){
-    clearTimeout(updateCheckTimeoutID);
-    updateCheckTimeoutID = setTimeout($TDU.triggerUpdateCheck, getTimeUntilNextHour(60*30)); // 30 minute offset
-    
-    $.getJSON(allowPre ? updateCheckUrlAll : updateCheckUrlLatest, function(response){
-      var release = allowPre ? response[0] : response;
-      
-      var tagName = release.tag_name;
-      var hasUpdate = tagName !== versionTag && tagName !== dismissedVersionTag && release.assets.length > 0;
-      
-      if (hasUpdate){
-        var obj = release.assets.find(asset => asset.name === updateFileName) || { browser_download_url: "" };
-        displayNotification(tagName, obj.browser_download_url, markdown(release.body));
-        
-        if (eventID){ // ignore undefined and 0
-          $TDU.onUpdateCheckFinished(eventID, tagName, obj.browser_download_url);
-        }
-      }
-      else if (eventID){ // ignore undefined and 0
-        $TDU.onUpdateCheckFinished(eventID, null, null);
-      }
-    });
-  };
-  
   //
   // Block: Check updates on startup.
   //
@@ -310,5 +244,5 @@
   //
   // Block: Setup global functions.
   //
-  window.TDUF_runUpdateCheck = runUpdateCheck;
+  window.TDUF_displayNotification = displayNotification;
 })($, $TDU);
diff --git a/TweetDuck.csproj b/TweetDuck.csproj
index fd09944d..9305dab0 100644
--- a/TweetDuck.csproj
+++ b/TweetDuck.csproj
@@ -69,6 +69,7 @@
     <Reference Include="System.Core" />
     <Reference Include="System.Drawing" />
     <Reference Include="System.Management" />
+    <Reference Include="System.Web.Extensions" />
     <Reference Include="System.Windows.Forms" />
   </ItemGroup>
   <ItemGroup>
@@ -316,6 +317,7 @@
     <Compile Include="Core\Management\BrowserCache.cs" />
     <Compile Include="Core\Utils\BrowserUtils.cs" />
     <Compile Include="Core\Utils\NativeMethods.cs" />
+    <Compile Include="Updates\UpdateCheckClient.cs" />
     <Compile Include="Updates\UpdateDownloadStatus.cs" />
     <Compile Include="Updates\UpdateHandler.cs" />
     <Compile Include="Updates\UpdateInfo.cs" />
diff --git a/Updates/Events/UpdateCheckEventArgs.cs b/Updates/Events/UpdateCheckEventArgs.cs
index b7f58861..5da32326 100644
--- a/Updates/Events/UpdateCheckEventArgs.cs
+++ b/Updates/Events/UpdateCheckEventArgs.cs
@@ -1,13 +1,14 @@
 using System;
+using TweetDuck.Data;
 
 namespace TweetDuck.Updates.Events{
     sealed class UpdateCheckEventArgs : EventArgs{
         public int EventId { get; }
-        public bool IsUpdateAvailable { get; }
+        public Result<UpdateInfo> Result { get; }
 
-        public UpdateCheckEventArgs(int eventId, bool isUpdateAvailable){
+        public UpdateCheckEventArgs(int eventId, Result<UpdateInfo> result){
             this.EventId = eventId;
-            this.IsUpdateAvailable = isUpdateAvailable;
+            this.Result = result;
         }
     }
 }
diff --git a/Updates/FormUpdateDownload.cs b/Updates/FormUpdateDownload.cs
index ab8de653..9b85519f 100644
--- a/Updates/FormUpdateDownload.cs
+++ b/Updates/FormUpdateDownload.cs
@@ -1,7 +1,5 @@
 using System;
 using System.Windows.Forms;
-using TweetDuck.Core.Other;
-using TweetDuck.Core.Utils;
 
 namespace TweetDuck.Updates{
     sealed partial class FormUpdateDownload : Form{
@@ -22,19 +20,8 @@ private void btnCancel_Click(object sender, EventArgs e){
         }
 
         private void timerDownloadCheck_Tick(object sender, EventArgs e){
-            if (updateInfo.DownloadStatus == UpdateDownloadStatus.Done){
+            if (updateInfo.DownloadStatus.IsFinished()){
                 timerDownloadCheck.Stop();
-                DialogResult = DialogResult.OK;
-                Close();
-            }
-            else if (updateInfo.DownloadStatus == UpdateDownloadStatus.Failed){
-                timerDownloadCheck.Stop();
-
-                if (FormMessage.Error("Update Has Failed", "Could not download the update: "+(updateInfo.DownloadError?.Message ?? "unknown error")+"\n\nDo you want to open the website and try downloading the update manually?", FormMessage.Yes, FormMessage.No)){
-                    BrowserUtils.OpenExternalBrowser(Program.Website);
-                    DialogResult = DialogResult.OK;
-                }
-                
                 Close();
             }
         }
diff --git a/Updates/UpdateCheckClient.cs b/Updates/UpdateCheckClient.cs
new file mode 100644
index 00000000..015eb50e
--- /dev/null
+++ b/Updates/UpdateCheckClient.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using System.Web.Script.Serialization;
+using TweetDuck.Core.Utils;
+using JsonObject = System.Collections.Generic.IDictionary<string, object>;
+
+namespace TweetDuck.Updates{
+    sealed class UpdateCheckClient{
+        private const string ApiLatestRelease = "https://api.github.com/repos/chylex/TweetDuck/releases/latest";
+        private const string UpdaterAssetName = "TweetDuck.Update.exe";
+
+        private readonly UpdaterSettings settings;
+
+        public UpdateCheckClient(UpdaterSettings settings){
+            this.settings = settings;
+        }
+
+        public Task<UpdateInfo> Check(){
+            TaskCompletionSource<UpdateInfo> result = new TaskCompletionSource<UpdateInfo>();
+
+            WebClient client = BrowserUtils.CreateWebClient();
+            client.Headers[HttpRequestHeader.Accept] = "application/vnd.github.v3+json"; // TODO could use .html to avoid custom markdown parsing
+
+            client.DownloadStringTaskAsync(ApiLatestRelease).ContinueWith(task => {
+                if (task.IsCanceled){
+                    result.SetCanceled();
+                }
+                else if (task.IsFaulted){
+                    result.SetException(task.Exception.InnerException);
+                }
+                else{
+                    try{
+                        result.SetResult(ParseFromJson(task.Result));
+                    }catch(Exception e){
+                        result.SetException(e);
+                    }
+                }
+            });
+            
+            return result.Task;
+        }
+
+        private UpdateInfo ParseFromJson(string json){
+            bool IsUpdaterAsset(JsonObject obj){
+                return UpdaterAssetName == (string)obj["name"];
+            }
+
+            string AssetDownloadUrl(JsonObject obj){
+                return (string)obj["browser_download_url"];
+            }
+            
+            JsonObject root = (JsonObject)new JavaScriptSerializer().DeserializeObject(json);
+
+            string versionTag = (string)root["tag_name"];
+            string releaseNotes = (string)root["body"];
+            string downloadUrl = ((Array)root["assets"]).Cast<JsonObject>().Where(IsUpdaterAsset).Select(AssetDownloadUrl).FirstOrDefault();
+
+            return new UpdateInfo(settings, versionTag, releaseNotes, downloadUrl);
+        }
+    }
+}
diff --git a/Updates/UpdateDownloadStatus.cs b/Updates/UpdateDownloadStatus.cs
index 3a10f455..7a47aad9 100644
--- a/Updates/UpdateDownloadStatus.cs
+++ b/Updates/UpdateDownloadStatus.cs
@@ -2,7 +2,14 @@
     public enum UpdateDownloadStatus{
         None = 0,
         InProgress,
+        AssetMissing,
         Done,
         Failed
     }
+
+    public static class UpdateDownloadStatusExtensions{
+        public static bool IsFinished(this UpdateDownloadStatus status){
+            return status == UpdateDownloadStatus.AssetMissing || status == UpdateDownloadStatus.Done || status == UpdateDownloadStatus.Failed;
+        }
+    }
 }
diff --git a/Updates/UpdateHandler.cs b/Updates/UpdateHandler.cs
index 6682ab5f..987ef2b6 100644
--- a/Updates/UpdateHandler.cs
+++ b/Updates/UpdateHandler.cs
@@ -1,21 +1,26 @@
 using CefSharp;
 using System;
+using System.Text;
+using System.Threading.Tasks;
 using System.Windows.Forms;
 using TweetDuck.Core.Controls;
 using TweetDuck.Core.Other.Interfaces;
-using TweetDuck.Core.Utils;
+using TweetDuck.Data;
 using TweetDuck.Resources;
 using TweetDuck.Updates.Events;
 
 namespace TweetDuck.Updates{
-    sealed class UpdateHandler{
+    sealed class UpdateHandler : IDisposable{
         public const int CheckCodeUpdatesDisabled = -1;
         public const int CheckCodeNotOnTweetDeck = -2;
-
-        private readonly ITweetDeckBrowser browser;
+        
         private readonly UpdaterSettings settings;
+        private readonly UpdateCheckClient client;
+        private readonly ITweetDeckBrowser browser;
+        private readonly Timer timer;
 
         public event EventHandler<UpdateEventArgs> UpdateAccepted;
+        public event EventHandler<UpdateEventArgs> UpdateDelayed;
         public event EventHandler<UpdateEventArgs> UpdateDismissed;
         public event EventHandler<UpdateCheckEventArgs> CheckFinished;
 
@@ -23,11 +28,44 @@ sealed class UpdateHandler{
         private UpdateInfo lastUpdateInfo;
 
         public UpdateHandler(ITweetDeckBrowser browser, UpdaterSettings settings){
-            this.browser = browser;
             this.settings = settings;
+            this.client = new UpdateCheckClient(settings);
+            
+            this.browser = browser;
+            this.browser.OnFrameLoaded(OnFrameLoaded);
+            this.browser.RegisterBridge("$TDU", new Bridge(this));
 
-            browser.OnFrameLoaded(OnFrameLoaded);
-            browser.RegisterBridge("$TDU", new Bridge(this));
+            this.timer = new Timer();
+            this.timer.Tick += timer_Tick;
+        }
+
+        public void Dispose(){
+            timer.Dispose();
+        }
+
+        private void timer_Tick(object sender, EventArgs e){
+            timer.Stop();
+            Check(false);
+        }
+
+        public void StartTimer(){
+            if (timer.Enabled){
+                return;
+            }
+
+            timer.Stop();
+
+            if (Program.UserConfig.EnableUpdateCheck){
+                DateTime now = DateTime.Now;
+                TimeSpan nextHour = now.AddSeconds(60*(60-now.Minute)-now.Second)-now;
+
+                if (nextHour.TotalMinutes < 15){
+                    nextHour = nextHour.Add(TimeSpan.FromHours(1));
+                }
+
+                timer.Interval = (int)Math.Ceiling(nextHour.TotalMilliseconds);
+                timer.Start();
+            }
         }
 
         private void OnFrameLoaded(IFrame frame){
@@ -43,17 +81,24 @@ public int Check(bool force){
                 if (!browser.IsTweetDeckWebsite){
                     return CheckCodeNotOnTweetDeck;
                 }
+                
+                int nextEventId = unchecked(++lastEventId);
+                Task<UpdateInfo> checkTask = client.Check();
 
-                browser.ExecuteFunction("TDUF_runUpdateCheck", (int)unchecked(++lastEventId), Program.VersionTag, settings.DismissedUpdate ?? string.Empty, settings.AllowPreReleases);
-                return lastEventId;
+                checkTask.ContinueWith(task => HandleUpdateCheckSuccessful(nextEventId, task.Result), TaskContinuationOptions.OnlyOnRanToCompletion);
+                checkTask.ContinueWith(task => HandleUpdateCheckFailed(nextEventId, task.Exception.InnerException), TaskContinuationOptions.OnlyOnFaulted);
+
+                return nextEventId;
             }
 
             return CheckCodeUpdatesDisabled;
         }
 
-        public void BeginUpdateDownload(Form ownerForm, UpdateInfo updateInfo, Action<UpdateInfo> onSuccess){
-            if (updateInfo.DownloadStatus == UpdateDownloadStatus.Done){
-                onSuccess(updateInfo);
+        public void BeginUpdateDownload(Form ownerForm, UpdateInfo updateInfo, Action<UpdateInfo> onFinished){
+            UpdateDownloadStatus status = updateInfo.DownloadStatus;
+
+            if (status == UpdateDownloadStatus.Done || status == UpdateDownloadStatus.AssetMissing){
+                onFinished(updateInfo);
             }
             else{
                 FormUpdateDownload downloadForm = new FormUpdateDownload(updateInfo);
@@ -65,13 +110,7 @@ public void BeginUpdateDownload(Form ownerForm, UpdateInfo updateInfo, Action<Up
 
                 downloadForm.FormClosed += (sender, args) => {
                     downloadForm.Dispose();
-                    
-                    if (downloadForm.DialogResult == DialogResult.OK){ // success or manual download
-                        onSuccess(updateInfo);
-                    }
-                    else{
-                        ownerForm.Show();
-                    }
+                    onFinished(updateInfo);
                 };
 
                 downloadForm.Show();
@@ -85,17 +124,41 @@ public void CleanupDownload(){
             }
         }
 
-        private void TriggerUpdateAcceptedEvent(UpdateEventArgs args){
-            UpdateAccepted?.Invoke(this, args);
+        private void HandleUpdateCheckSuccessful(int eventId, UpdateInfo info){
+            if (info.IsUpdateNew && !info.IsUpdateDismissed){
+                CleanupDownload();
+                lastUpdateInfo = info;
+                lastUpdateInfo.BeginSilentDownload();
+
+                browser.ExecuteFunction("TDUF_displayNotification", lastUpdateInfo.VersionTag, Convert.ToBase64String(Encoding.GetEncoding("iso-8859-1").GetBytes(lastUpdateInfo.ReleaseNotes))); // TODO move browser stuff outside
+            }
+            
+            CheckFinished?.Invoke(this, new UpdateCheckEventArgs(eventId, new Result<UpdateInfo>(info)));
         }
 
-        private void TriggerUpdateDismissedEvent(UpdateEventArgs args){
-            settings.DismissedUpdate = args.UpdateInfo.VersionTag;
-            UpdateDismissed?.Invoke(this, args);
+        private void HandleUpdateCheckFailed(int eventId, Exception exception){
+            CheckFinished?.Invoke(this, new UpdateCheckEventArgs(eventId, new Result<UpdateInfo>(exception)));
         }
 
-        private void TriggerCheckFinishedEvent(UpdateCheckEventArgs args){
-            CheckFinished?.Invoke(this, args);
+        private void TriggerUpdateAcceptedEvent(){
+            if (lastUpdateInfo != null){
+                UpdateAccepted?.Invoke(this, new UpdateEventArgs(lastUpdateInfo));
+            }
+        }
+
+        private void TriggerUpdateDelayedEvent(){
+            if (lastUpdateInfo != null){
+                UpdateDelayed?.Invoke(this, new UpdateEventArgs(lastUpdateInfo));
+            }
+        }
+
+        private void TriggerUpdateDismissedEvent(){
+            if (lastUpdateInfo != null){
+                settings.DismissedUpdate = lastUpdateInfo.VersionTag;
+                UpdateDismissed?.Invoke(this, new UpdateEventArgs(lastUpdateInfo));
+
+                CleanupDownload();
+            }
         }
 
         public sealed class Bridge{
@@ -109,31 +172,16 @@ public void TriggerUpdateCheck(){
                 owner.Check(false);
             }
 
-            public void OnUpdateCheckFinished(int eventId, string versionTag, string downloadUrl){
-                if (versionTag != null && (owner.lastUpdateInfo == null || owner.lastUpdateInfo.VersionTag != versionTag)){
-                    owner.CleanupDownload();
-                    owner.lastUpdateInfo = new UpdateInfo(owner.settings, eventId, versionTag, downloadUrl);
-                    owner.lastUpdateInfo.BeginSilentDownload();
-                }
-                
-                owner.TriggerCheckFinishedEvent(new UpdateCheckEventArgs(eventId, owner.lastUpdateInfo != null));
+            public void OnUpdateAccepted(){
+                owner.TriggerUpdateAcceptedEvent();
             }
 
-            public void OnUpdateAccepted(){
-                if (owner.lastUpdateInfo != null){
-                    owner.TriggerUpdateAcceptedEvent(new UpdateEventArgs(owner.lastUpdateInfo));
-                }
+            public void OnUpdateDelayed(){
+                owner.TriggerUpdateDelayedEvent();
             }
 
             public void OnUpdateDismissed(){
-                if (owner.lastUpdateInfo != null){
-                    owner.TriggerUpdateDismissedEvent(new UpdateEventArgs(owner.lastUpdateInfo));
-                    owner.CleanupDownload();
-                }
-            }
-
-            public void OpenBrowser(string url){
-                BrowserUtils.OpenExternalBrowser(url);
+                owner.TriggerUpdateDismissedEvent();
             }
         }
     }
diff --git a/Updates/UpdateInfo.cs b/Updates/UpdateInfo.cs
index c3d7cbd8..c7d02c74 100644
--- a/Updates/UpdateInfo.cs
+++ b/Updates/UpdateInfo.cs
@@ -5,40 +5,43 @@
 
 namespace TweetDuck.Updates{
     sealed class UpdateInfo{
-        public int EventId { get; }
         public string VersionTag { get; }
+        public string ReleaseNotes { get; }
         public string InstallerPath { get; }
 
+        public bool IsUpdateNew => VersionTag != Program.VersionTag;
+        public bool IsUpdateDismissed => VersionTag == settings.DismissedUpdate;
+
         public UpdateDownloadStatus DownloadStatus { get; private set; }
         public Exception DownloadError { get; private set; }
 
-        private readonly string installerFolder;
+        private readonly UpdaterSettings settings;
         private readonly string downloadUrl;
         private WebClient currentDownload;
 
-        public UpdateInfo(UpdaterSettings settings, int eventId, string versionTag, string downloadUrl){
-            this.installerFolder = settings.InstallerDownloadFolder;
+        public UpdateInfo(UpdaterSettings settings, string versionTag, string releaseNotes, string downloadUrl){
+            this.settings = settings;
             this.downloadUrl = downloadUrl;
-
-            this.EventId = eventId;
+            
             this.VersionTag = versionTag;
-            this.InstallerPath = Path.Combine(installerFolder, "TweetDuck."+versionTag+".exe");
+            this.ReleaseNotes = releaseNotes;
+            this.InstallerPath = Path.Combine(settings.InstallerDownloadFolder, "TweetDuck."+versionTag+".exe");
         }
 
         public void BeginSilentDownload(){
             if (DownloadStatus == UpdateDownloadStatus.None || DownloadStatus == UpdateDownloadStatus.Failed){
                 DownloadStatus = UpdateDownloadStatus.InProgress;
 
-                try{
-                    Directory.CreateDirectory(installerFolder);
-                }catch(Exception e){
-                    DownloadError = e;
-                    DownloadStatus = UpdateDownloadStatus.Failed;
+                if (string.IsNullOrEmpty(downloadUrl)){
+                    DownloadError = new InvalidDataException("Missing installer asset.");
+                    DownloadStatus = UpdateDownloadStatus.AssetMissing;
                     return;
                 }
 
-                if (string.IsNullOrEmpty(downloadUrl)){
-                    DownloadError = new UriFormatException("Could not determine URL of the update installer");
+                try{
+                    Directory.CreateDirectory(settings.InstallerDownloadFolder);
+                }catch(Exception e){
+                    DownloadError = e;
                     DownloadStatus = UpdateDownloadStatus.Failed;
                     return;
                 }
@@ -68,5 +71,13 @@ public void DeleteInstaller(){
                 // rip
             }
         }
+
+        public override bool Equals(object obj){
+            return obj is UpdateInfo info && VersionTag == info.VersionTag;
+        }
+
+        public override int GetHashCode(){
+            return VersionTag.GetHashCode();
+        }
     }
 }