Major overhaul #2
@@ -15,33 +15,35 @@ namespace ntfysh_client
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    public partial class Form1 : Form
 | 
					    public partial class Form1 : Form
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private NotificationListener notificationListener;
 | 
					        private readonly NotificationListener _notificationListener;
 | 
				
			||||||
 | 
					        private bool _trueExit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public Form1()
 | 
					        public Form1(NotificationListener notificationListener)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            notificationListener = new NotificationListener();
 | 
					            _notificationListener = notificationListener;
 | 
				
			||||||
            notificationListener.OnNotificationReceive += OnNotificationReceive;
 | 
					            _notificationListener.OnNotificationReceive += OnNotificationReceive;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
            InitializeComponent();
 | 
					            InitializeComponent();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void Form1_Load(object sender, EventArgs e)
 | 
					        private void Form1_Load(object sender, EventArgs e) => LoadTopics();
 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            this.LoadTopics();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void subscribeNewTopic_Click(object sender, EventArgs e)
 | 
					        private void subscribeNewTopic_Click(object sender, EventArgs e)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            using (var dialog = new SubscribeDialog(notificationTopics))
 | 
					            using SubscribeDialog dialog = new SubscribeDialog(notificationTopics);
 | 
				
			||||||
            {
 | 
					            DialogResult result = dialog.ShowDialog();
 | 
				
			||||||
                var result = dialog.ShowDialog();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (result == DialogResult.OK)
 | 
					            //Do not subscribe on cancelled dialog
 | 
				
			||||||
                {
 | 
					            if (result != DialogResult.OK) return;
 | 
				
			||||||
                    notificationListener.SubscribeToTopic(dialog.getUniqueString(), dialog.getTopicId(), dialog.getServerUrl(), dialog.getUsername(), dialog.getPassword());
 | 
					                
 | 
				
			||||||
                    notificationTopics.Items.Add(dialog.getUniqueString());
 | 
					            //Subscribe
 | 
				
			||||||
                    this.SaveTopicsToFile();
 | 
					            _notificationListener.SubscribeToTopicUsingLongHttpJson(dialog.Unique, dialog.TopicId, dialog.ServerUrl, dialog.Username, dialog.Password);
 | 
				
			||||||
                }
 | 
					                    
 | 
				
			||||||
            }
 | 
					            //Add to the user visible list
 | 
				
			||||||
 | 
					            notificationTopics.Items.Add(dialog.Unique);
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					            //Save the topics persistently
 | 
				
			||||||
 | 
					            SaveTopicsToFile();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void removeSelectedTopics_Click(object sender, EventArgs e)
 | 
					        private void removeSelectedTopics_Click(object sender, EventArgs e)
 | 
				
			||||||
@@ -50,11 +52,11 @@ namespace ntfysh_client
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                string topicUniqueString = (string)notificationTopics.Items[notificationTopics.SelectedIndex];
 | 
					                string topicUniqueString = (string)notificationTopics.Items[notificationTopics.SelectedIndex];
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                notificationListener.RemoveTopicByUniqueString(topicUniqueString);
 | 
					                _notificationListener.UnsubscribeFromTopic(topicUniqueString);
 | 
				
			||||||
                notificationTopics.Items.Remove(topicUniqueString);
 | 
					                notificationTopics.Items.Remove(topicUniqueString);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            this.SaveTopicsToFile();
 | 
					            SaveTopicsToFile();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void notificationTopics_SelectedValueChanged(object sender, EventArgs e)
 | 
					        private void notificationTopics_SelectedValueChanged(object sender, EventArgs e)
 | 
				
			||||||
@@ -64,51 +66,48 @@ namespace ntfysh_client
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        private void notificationTopics_Click(object sender, EventArgs e)
 | 
					        private void notificationTopics_Click(object sender, EventArgs e)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var ev = (MouseEventArgs)e;
 | 
					            MouseEventArgs ev = (MouseEventArgs)e;
 | 
				
			||||||
            var clickedItemIndex = notificationTopics.IndexFromPoint(new Point(ev.X, ev.Y));
 | 
					            int clickedItemIndex = notificationTopics.IndexFromPoint(new Point(ev.X, ev.Y));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (clickedItemIndex == -1)
 | 
					            if (clickedItemIndex == -1) notificationTopics.ClearSelected();
 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                notificationTopics.ClearSelected();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void button1_Click(object sender, EventArgs e)
 | 
					        private void button1_Click(object sender, EventArgs e)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            this.Visible = false;
 | 
					            Visible = false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void notifyIcon_Click(object sender, EventArgs e)
 | 
					        private void notifyIcon_Click(object sender, EventArgs e)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var mouseEv = (MouseEventArgs)e;
 | 
					            MouseEventArgs mouseEv = (MouseEventArgs)e;
 | 
				
			||||||
            if (mouseEv.Button == MouseButtons.Left)
 | 
					            
 | 
				
			||||||
            {
 | 
					            if (mouseEv.Button != MouseButtons.Left) return;
 | 
				
			||||||
                this.Visible = !this.Visible;
 | 
					            
 | 
				
			||||||
                this.BringToFront();
 | 
					            Visible = !Visible;
 | 
				
			||||||
            }
 | 
					            BringToFront();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void showControlWindowToolStripMenuItem_Click(object sender, EventArgs e)
 | 
					        private void showControlWindowToolStripMenuItem_Click(object sender, EventArgs e)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            this.Visible = true;
 | 
					            Visible = true;
 | 
				
			||||||
            this.BringToFront();
 | 
					            BringToFront();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private string GetTopicsFilePath()
 | 
					        private string GetTopicsFilePath()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            string binaryDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
 | 
					            string binaryDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? throw new InvalidOperationException("Unable to determine path for application");
 | 
				
			||||||
            return Path.Combine(binaryDirectory ?? throw new InvalidOperationException("Unable to determine path for topics file"), "topics.json");
 | 
					            return Path.Combine(binaryDirectory ?? throw new InvalidOperationException("Unable to determine path for topics file"), "topics.json");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        private string GetLegacyTopicsFilePath()
 | 
					        private string GetLegacyTopicsFilePath()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            string binaryDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
 | 
					            string binaryDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? throw new InvalidOperationException("Unable to determine path for application");
 | 
				
			||||||
            return Path.Combine(binaryDirectory ?? throw new InvalidOperationException("Unable to determine path for legacy topics file"), "topics.txt");
 | 
					            return Path.Combine(binaryDirectory ?? throw new InvalidOperationException("Unable to determine path for legacy topics file"), "topics.txt");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void SaveTopicsToFile()
 | 
					        private void SaveTopicsToFile()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            string topicsSerialised = JsonConvert.SerializeObject(notificationListener.SubscribedTopicsByUnique.Select(st => st.Value).ToList(), Formatting.Indented);
 | 
					            string topicsSerialised = JsonConvert.SerializeObject(_notificationListener.SubscribedTopicsByUnique.Select(st => st.Value).ToList(), Formatting.Indented);
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            File.WriteAllText(GetTopicsFilePath(), topicsSerialised);
 | 
					            File.WriteAllText(GetTopicsFilePath(), topicsSerialised);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -128,13 +127,13 @@ namespace ntfysh_client
 | 
				
			|||||||
                {
 | 
					                {
 | 
				
			||||||
                    while (!reader.EndOfStream)
 | 
					                    while (!reader.EndOfStream)
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        string legacyTopic = reader.ReadLine();
 | 
					                        string? legacyTopic = reader.ReadLine();
 | 
				
			||||||
                        legacyTopics.Add(legacyTopic);
 | 
					                        if (!string.IsNullOrWhiteSpace(legacyTopic)) legacyTopics.Add(legacyTopic);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                //Assemble new format
 | 
					                //Assemble new format
 | 
				
			||||||
                List<SubscribedTopic> newTopics = legacyTopics.Select(lt => new SubscribedTopic(lt, "https://ntfy.sh", null, null, null)).ToList();
 | 
					                List<SubscribedTopic> newTopics = legacyTopics.Select(lt => new SubscribedTopic(lt, "https://ntfy.sh", null, null, null, null)).ToList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                string newFormatSerialised = JsonConvert.SerializeObject(newTopics, Formatting.Indented);
 | 
					                string newFormatSerialised = JsonConvert.SerializeObject(newTopics, Formatting.Indented);
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
@@ -160,9 +159,9 @@ namespace ntfysh_client
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            //Deserialise the topics
 | 
					            //Deserialise the topics
 | 
				
			||||||
            List<SubscribedTopic> topics = JsonConvert.DeserializeObject<List<SubscribedTopic>>(topicsSerialised);
 | 
					            List<SubscribedTopic>? topics = JsonConvert.DeserializeObject<List<SubscribedTopic>>(topicsSerialised);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (topics == null)
 | 
					            if (topics is null)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                //TODO Deserialise error!
 | 
					                //TODO Deserialise error!
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
@@ -171,7 +170,7 @@ namespace ntfysh_client
 | 
				
			|||||||
            //Load them in
 | 
					            //Load them in
 | 
				
			||||||
            foreach (SubscribedTopic topic in topics)
 | 
					            foreach (SubscribedTopic topic in topics)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                notificationListener.SubscribeToTopic($"{topic.TopicId}@{topic.ServerUrl}", topic.TopicId, topic.ServerUrl, topic.Username, topic.Password);
 | 
					                _notificationListener.SubscribeToTopicUsingLongHttpJson($"{topic.TopicId}@{topic.ServerUrl}", topic.TopicId, topic.ServerUrl, topic.Username, topic.Password);
 | 
				
			||||||
                notificationTopics.Items.Add($"{topic.TopicId}@{topic.ServerUrl}");
 | 
					                notificationTopics.Items.Add($"{topic.TopicId}@{topic.ServerUrl}");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -185,24 +184,22 @@ namespace ntfysh_client
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            notifyIcon.Dispose();
 | 
					            notifyIcon.Dispose();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        private bool trueExit = false;
 | 
					 | 
				
			||||||
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
 | 
					        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            // Let it close
 | 
					            // Let it close
 | 
				
			||||||
            if (trueExit) return;
 | 
					            if (_trueExit) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (e.CloseReason == CloseReason.UserClosing)
 | 
					            if (e.CloseReason != CloseReason.UserClosing) return;
 | 
				
			||||||
            {
 | 
					            
 | 
				
			||||||
                this.Visible = false;
 | 
					            Visible = false;
 | 
				
			||||||
                e.Cancel = true;
 | 
					            e.Cancel = true;
 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void exitToolStripMenuItem_Click(object sender, EventArgs e)
 | 
					        private void exitToolStripMenuItem_Click(object sender, EventArgs e)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            trueExit = true;
 | 
					            _trueExit = true;
 | 
				
			||||||
            this.Close();
 | 
					            Close();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void ntfyshWebsiteToolStripMenuItem_Click(object sender, EventArgs e)
 | 
					        private void ntfyshWebsiteToolStripMenuItem_Click(object sender, EventArgs e)
 | 
				
			||||||
@@ -212,9 +209,8 @@ namespace ntfysh_client
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
 | 
					        private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var d = new AboutBox();
 | 
					            using AboutBox d = new AboutBox();
 | 
				
			||||||
            d.ShowDialog();
 | 
					            d.ShowDialog();
 | 
				
			||||||
            d.Dispose();
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,7 @@ using System.Linq;
 | 
				
			|||||||
using System.Net;
 | 
					using System.Net;
 | 
				
			||||||
using System.Net.Http;
 | 
					using System.Net.Http;
 | 
				
			||||||
using System.Net.Http.Headers;
 | 
					using System.Net.Http.Headers;
 | 
				
			||||||
 | 
					using System.Net.WebSockets;
 | 
				
			||||||
using System.Text;
 | 
					using System.Text;
 | 
				
			||||||
using System.Threading;
 | 
					using System.Threading;
 | 
				
			||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
@@ -14,153 +15,147 @@ using System.Web;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace ntfysh_client
 | 
					namespace ntfysh_client
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    class NotificationListener : IDisposable
 | 
					    public class NotificationListener : IDisposable
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private HttpClient httpClient;
 | 
					        private readonly HttpClient _httpClient = new();
 | 
				
			||||||
        
 | 
					        private bool _isDisposed;
 | 
				
			||||||
        private bool disposedValue;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public readonly Dictionary<string, SubscribedTopic> SubscribedTopicsByUnique = new Dictionary<string, SubscribedTopic>();
 | 
					        public readonly Dictionary<string, SubscribedTopic> SubscribedTopicsByUnique = new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public delegate void NotificationReceiveHandler(object sender, NotificationReceiveEventArgs e);
 | 
					        public delegate void NotificationReceiveHandler(object sender, NotificationReceiveEventArgs e);
 | 
				
			||||||
        public event NotificationReceiveHandler OnNotificationReceive;
 | 
					        public event NotificationReceiveHandler? OnNotificationReceive;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public NotificationListener()
 | 
					        public NotificationListener()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            httpClient = new HttpClient();
 | 
					            _httpClient.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite);
 | 
				
			||||||
 | 
					 | 
				
			||||||
            httpClient.Timeout = TimeSpan.FromMilliseconds(Timeout.Infinite);
 | 
					 | 
				
			||||||
            ServicePointManager.DefaultConnectionLimit = 100;
 | 
					            ServicePointManager.DefaultConnectionLimit = 100;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public async Task SubscribeToTopic(string unique, string topicId, string serverUrl, string username, string password)
 | 
					        private async Task ListenToTopicAsync(HttpRequestMessage message, CancellationToken cancellationToken)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
					            if (_isDisposed) throw new ObjectDisposedException(nameof(NotificationListener));
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            while (!cancellationToken.IsCancellationRequested)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                using HttpResponseMessage response = await _httpClient.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
 | 
				
			||||||
 | 
					                using Stream body = await response.Content.ReadAsStreamAsync();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					                try
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    StringBuilder mainBuffer = new();
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    while (!cancellationToken.IsCancellationRequested)
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        //Read as much as possible
 | 
				
			||||||
 | 
					                        byte[] buffer = new byte[8192];
 | 
				
			||||||
 | 
					                        int readBytes = await body.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        //Append it to our main buffer
 | 
				
			||||||
 | 
					                        mainBuffer.Append(Encoding.UTF8.GetString(buffer, 0, readBytes));
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        List<string> lines = mainBuffer.ToString().Split('\n').ToList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        //If we have not yet received a full line, meaning theres only 1 part, go back to reading
 | 
				
			||||||
 | 
					                        if (lines.Count <= 1) continue;
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        //We now have at least 1 line! Count how many full lines. There will always be a partial line at the end, even if that partial line is empty
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        //Separate the partial line from the full lines
 | 
				
			||||||
 | 
					                        int partialLineIndex = lines.Count - 1;
 | 
				
			||||||
 | 
					                        string partialLine = lines[partialLineIndex];
 | 
				
			||||||
 | 
					                        lines.RemoveAt(partialLineIndex);
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        //Process the full lines
 | 
				
			||||||
 | 
					                        foreach (string line in lines) ProcessMessage(line);
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                        //Write back the partial line
 | 
				
			||||||
 | 
					                        mainBuffer.Clear();
 | 
				
			||||||
 | 
					                        mainBuffer.Append(partialLine);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                catch (Exception ex)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    #if DEBUG
 | 
				
			||||||
 | 
					                        Debug.WriteLine(ex);
 | 
				
			||||||
 | 
					                    #endif
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    //Fall back to the outer loop to restart the listen, or cancel if requested
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private void ProcessMessage(string message)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            #if DEBUG
 | 
				
			||||||
 | 
					                Debug.WriteLine(message);
 | 
				
			||||||
 | 
					            #endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            NtfyEvent? evt = JsonConvert.DeserializeObject<NtfyEvent>(message);
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					            //If we hit this, ntfy sent us an invalid message
 | 
				
			||||||
 | 
					            if (evt is null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (evt.Event == "message")
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                OnNotificationReceive?.Invoke(this, new NotificationReceiveEventArgs(evt.Title, evt.Message));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public void SubscribeToTopicUsingLongHttpJson(string unique, string topicId, string serverUrl, string? username, string? password)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if (_isDisposed) throw new ObjectDisposedException(nameof(NotificationListener));
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (SubscribedTopicsByUnique.ContainsKey(unique)) throw new InvalidOperationException("A topic with this unique already exists");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
            if (string.IsNullOrWhiteSpace(username)) username = null;
 | 
					            if (string.IsNullOrWhiteSpace(username)) username = null;
 | 
				
			||||||
            if (string.IsNullOrWhiteSpace(password)) password = null;
 | 
					            if (string.IsNullOrWhiteSpace(password)) password = null;
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            HttpRequestMessage msg = new HttpRequestMessage(HttpMethod.Get, $"{serverUrl}/{HttpUtility.UrlEncode(topicId)}/json");
 | 
					            HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, $"{serverUrl}/{HttpUtility.UrlEncode(topicId)}/json");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (username != null && password != null)
 | 
					            if (username != null && password != null)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                byte[] boundCredentialsBytes = Encoding.UTF8.GetBytes($"{username}:{password}");
 | 
					                byte[] boundCredentialsBytes = Encoding.UTF8.GetBytes($"{username}:{password}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                msg.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(boundCredentialsBytes));
 | 
					                message.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(boundCredentialsBytes));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            using (HttpResponseMessage response = await httpClient.SendAsync(msg, HttpCompletionOption.ResponseHeadersRead))
 | 
					            CancellationTokenSource listenCanceller = new();
 | 
				
			||||||
            {
 | 
					            Task listenTask = ListenToTopicAsync(message, listenCanceller.Token);
 | 
				
			||||||
                using (Stream body = await response.Content.ReadAsStreamAsync())
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    using (StreamReader reader = new StreamReader(body))
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        SubscribedTopicsByUnique.Add(unique, new SubscribedTopic(topicId, serverUrl, username, password, reader));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        try
 | 
					            SubscribedTopicsByUnique.Add(unique, new SubscribedTopic(topicId, serverUrl, username, password, listenTask, listenCanceller));
 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            // The loop will be broken when this stream is closed
 | 
					 | 
				
			||||||
                            while (true)
 | 
					 | 
				
			||||||
                            {
 | 
					 | 
				
			||||||
                                var line = await reader.ReadLineAsync();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                Debug.WriteLine(line);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                NtfyEventObject nev = JsonConvert.DeserializeObject<NtfyEventObject>(line);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                if (nev.Event == "message")
 | 
					 | 
				
			||||||
                                {
 | 
					 | 
				
			||||||
                                    if (OnNotificationReceive != null)
 | 
					 | 
				
			||||||
                                    {
 | 
					 | 
				
			||||||
                                        var evArgs = new NotificationReceiveEventArgs(nev.Title, nev.Message);
 | 
					 | 
				
			||||||
                                        OnNotificationReceive(this, evArgs);
 | 
					 | 
				
			||||||
                                    }
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        catch (Exception ex)
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            Debug.WriteLine(ex);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            // If the topic is still registered, then that stream wasn't mean to be closed (maybe network failure?)
 | 
					 | 
				
			||||||
                            // Restart it
 | 
					 | 
				
			||||||
                            if (SubscribedTopicsByUnique.ContainsKey(unique))
 | 
					 | 
				
			||||||
                            {
 | 
					 | 
				
			||||||
                                SubscribeToTopic(unique, topicId, serverUrl, username, password);
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public void RemoveTopicByUniqueString(string topicUniqueString)
 | 
					        public void UnsubscribeFromTopic(string topicUniqueString)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Debug.WriteLine($"Removing topic {topicUniqueString}");
 | 
					            if (_isDisposed) throw new ObjectDisposedException(nameof(NotificationListener));
 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (SubscribedTopicsByUnique.ContainsKey(topicUniqueString))
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                // Not moronic to store it in a variable; this solves a race condition in SubscribeToTopic
 | 
					 | 
				
			||||||
                SubscribedTopic topic = SubscribedTopicsByUnique[topicUniqueString];
 | 
					 | 
				
			||||||
                topic.Stream.Close();
 | 
					 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                SubscribedTopicsByUnique.Remove(topicUniqueString);
 | 
					            #if DEBUG
 | 
				
			||||||
            }
 | 
					                Debug.WriteLine($"Removing topic {topicUniqueString}");
 | 
				
			||||||
 | 
					            #endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            //Topic isn't even subscribed, ignore
 | 
				
			||||||
 | 
					            if (!SubscribedTopicsByUnique.TryGetValue(topicUniqueString, out SubscribedTopic topic)) return;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            //Cancel and dispose the task runner
 | 
				
			||||||
 | 
					            topic.RunnerCanceller.Cancel();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            //Wait for the task runner to shut down
 | 
				
			||||||
 | 
					            while (!topic.Runner.IsCompleted) Thread.Sleep(100);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            //Dispose task
 | 
				
			||||||
 | 
					            topic.Runner.Dispose();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            //Remove the old topic
 | 
				
			||||||
 | 
					            SubscribedTopicsByUnique.Remove(topicUniqueString);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected virtual void Dispose(bool disposing)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            if (!disposedValue)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                if (disposing)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    // TODO: dispose managed state (managed objects)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                // TODO: free unmanaged resources (unmanaged objects) and override finalizer
 | 
					 | 
				
			||||||
                // TODO: set large fields to null
 | 
					 | 
				
			||||||
                disposedValue = true;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
 | 
					 | 
				
			||||||
        // ~NotificationListener()
 | 
					 | 
				
			||||||
        // {
 | 
					 | 
				
			||||||
        //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 | 
					 | 
				
			||||||
        //     Dispose(disposing: false);
 | 
					 | 
				
			||||||
        // }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        public void Dispose()
 | 
					        public void Dispose()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
 | 
					            if (_isDisposed) return;
 | 
				
			||||||
            Dispose(disposing: true);
 | 
					            
 | 
				
			||||||
            GC.SuppressFinalize(this);
 | 
					            _httpClient.Dispose();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            _isDisposed = true;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    public class NotificationReceiveEventArgs : EventArgs
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        public string Title { get; private set; }
 | 
					 | 
				
			||||||
        public string Message { get; private set; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        public NotificationReceiveEventArgs(string title, string message)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            Title = title;
 | 
					 | 
				
			||||||
            Message = message;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public class NtfyEventObject
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        [JsonProperty("id")]
 | 
					 | 
				
			||||||
        public string Id { get; set; }
 | 
					 | 
				
			||||||
        [JsonProperty("time")]
 | 
					 | 
				
			||||||
        public Int64 Time { get; set; }
 | 
					 | 
				
			||||||
        [JsonProperty("event")]
 | 
					 | 
				
			||||||
        public string Event { get; set; }
 | 
					 | 
				
			||||||
        [JsonProperty("topic")]
 | 
					 | 
				
			||||||
        public string Topic { get; set; }
 | 
					 | 
				
			||||||
        [JsonProperty("message")]
 | 
					 | 
				
			||||||
        public string Message { get; set; }
 | 
					 | 
				
			||||||
        [JsonProperty("title")]
 | 
					 | 
				
			||||||
        public string Title { get; set; }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										16
									
								
								ntfysh_client/NotificationReceiveEventArgs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								ntfysh_client/NotificationReceiveEventArgs.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					using System;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace ntfysh_client
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public class NotificationReceiveEventArgs : EventArgs
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        public string Title { get; }
 | 
				
			||||||
 | 
					        public string Message { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public NotificationReceiveEventArgs(string title, string message)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Title = title;
 | 
				
			||||||
 | 
					            Message = message;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										25
									
								
								ntfysh_client/NtfyEvent.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								ntfysh_client/NtfyEvent.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					using Newtonsoft.Json;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace ntfysh_client
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public class NtfyEvent
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        [JsonProperty("id")]
 | 
				
			||||||
 | 
					        public string Id { get; set; } = null!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [JsonProperty("time")]
 | 
				
			||||||
 | 
					        public long Time { get; set; }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        [JsonProperty("event")]
 | 
				
			||||||
 | 
					        public string Event { get; set; } = null!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [JsonProperty("topic")]
 | 
				
			||||||
 | 
					        public string Topic { get; set; } = null!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [JsonProperty("message")]
 | 
				
			||||||
 | 
					        public string Message { get; set; } = null!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [JsonProperty("title")]
 | 
				
			||||||
 | 
					        public string Title { get; set; } = null!;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -8,6 +8,8 @@ namespace ntfysh_client
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    static class Program
 | 
					    static class Program
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        private static readonly NotificationListener NotificationListener = new NotificationListener();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
        /// The main entry point for the application.
 | 
					        /// The main entry point for the application.
 | 
				
			||||||
        /// </summary>
 | 
					        /// </summary>
 | 
				
			||||||
@@ -16,7 +18,7 @@ namespace ntfysh_client
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            Application.EnableVisualStyles();
 | 
					            Application.EnableVisualStyles();
 | 
				
			||||||
            Application.SetCompatibleTextRenderingDefault(false);
 | 
					            Application.SetCompatibleTextRenderingDefault(false);
 | 
				
			||||||
            Application.Run(new Form1());
 | 
					            Application.Run(new Form1(NotificationListener));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,16 @@ namespace ntfysh_client
 | 
				
			|||||||
    public partial class SubscribeDialog : Form
 | 
					    public partial class SubscribeDialog : Form
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private readonly ListBox _notificationTopics;
 | 
					        private readonly ListBox _notificationTopics;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public string TopicId => topicId.Text;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public string ServerUrl => serverUrl.Text;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public string Username => username.Text;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public string Password => password.Text;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public string Unique => $"{topicId.Text}@{serverUrl.Text}";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public SubscribeDialog(ListBox notificationTopics)
 | 
					        public SubscribeDialog(ListBox notificationTopics)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@@ -13,31 +23,6 @@ namespace ntfysh_client
 | 
				
			|||||||
            InitializeComponent();
 | 
					            InitializeComponent();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public string getTopicId()
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            return topicId.Text;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        public string getServerUrl()
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            return serverUrl.Text;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        public string getUsername()
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            return username.Text;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        public string getPassword()
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            return password.Text;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        public string getUniqueString()
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            return $"{topicId.Text}@{serverUrl.Text}";
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        private void button1_Click(object sender, EventArgs e)
 | 
					        private void button1_Click(object sender, EventArgs e)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (topicId.Text.Length < 1)
 | 
					            if (topicId.Text.Length < 1)
 | 
				
			||||||
@@ -72,7 +57,7 @@ namespace ntfysh_client
 | 
				
			|||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (_notificationTopics.Items.Contains(getUniqueString()))
 | 
					            if (_notificationTopics.Items.Contains(Unique))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                MessageBox.Show($"The specified topic '{topicId.Text}' on the server '{serverUrl.Text}' is already subscribed", "Topic already subscribed", MessageBoxButtons.OK, MessageBoxIcon.Error);
 | 
					                MessageBox.Show($"The specified topic '{topicId.Text}' on the server '{serverUrl.Text}' is already subscribed", "Topic already subscribed", MessageBoxButtons.OK, MessageBoxIcon.Error);
 | 
				
			||||||
                DialogResult = DialogResult.None;
 | 
					                DialogResult = DialogResult.None;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,25 +1,30 @@
 | 
				
			|||||||
using System.IO;
 | 
					using System.Threading;
 | 
				
			||||||
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
using Newtonsoft.Json;
 | 
					using Newtonsoft.Json;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace ntfysh_client
 | 
					namespace ntfysh_client
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public class SubscribedTopic
 | 
					    public class SubscribedTopic
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        public SubscribedTopic(string topicId, string serverUrl, string username, string password, StreamReader stream)
 | 
					        public SubscribedTopic(string topicId, string serverUrl, string? username, string? password, Task runner, CancellationTokenSource runnerCanceller)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            TopicId = topicId;
 | 
					            TopicId = topicId;
 | 
				
			||||||
            ServerUrl = serverUrl;
 | 
					            ServerUrl = serverUrl;
 | 
				
			||||||
            Username = username;
 | 
					            Username = username;
 | 
				
			||||||
            Password = password;
 | 
					            Password = password;
 | 
				
			||||||
            Stream = stream;
 | 
					            Runner = runner;
 | 
				
			||||||
 | 
					            RunnerCanceller = runnerCanceller;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public string TopicId { get; }
 | 
					        public string TopicId { get; }
 | 
				
			||||||
        public string ServerUrl { get; }
 | 
					        public string ServerUrl { get; }
 | 
				
			||||||
        public string Username { get; }
 | 
					        public string? Username { get; }
 | 
				
			||||||
        public string Password { get; }
 | 
					        public string? Password { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [JsonIgnore]
 | 
					        [JsonIgnore]
 | 
				
			||||||
        public StreamReader Stream { get; }
 | 
					        public Task Runner { get; }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        [JsonIgnore]
 | 
				
			||||||
 | 
					        public CancellationTokenSource RunnerCanceller { get; }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -12,6 +12,8 @@
 | 
				
			|||||||
    <FileAlignment>512</FileAlignment>
 | 
					    <FileAlignment>512</FileAlignment>
 | 
				
			||||||
    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
 | 
					    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
 | 
				
			||||||
    <Deterministic>true</Deterministic>
 | 
					    <Deterministic>true</Deterministic>
 | 
				
			||||||
 | 
					    <LangVersion>latest</LangVersion>
 | 
				
			||||||
 | 
					    <Nullable>enable</Nullable>
 | 
				
			||||||
  </PropertyGroup>
 | 
					  </PropertyGroup>
 | 
				
			||||||
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
 | 
					  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
 | 
				
			||||||
    <PlatformTarget>AnyCPU</PlatformTarget>
 | 
					    <PlatformTarget>AnyCPU</PlatformTarget>
 | 
				
			||||||
@@ -66,6 +68,8 @@
 | 
				
			|||||||
      <DependentUpon>Form1.cs</DependentUpon>
 | 
					      <DependentUpon>Form1.cs</DependentUpon>
 | 
				
			||||||
    </Compile>
 | 
					    </Compile>
 | 
				
			||||||
    <Compile Include="NotificationListener.cs" />
 | 
					    <Compile Include="NotificationListener.cs" />
 | 
				
			||||||
 | 
					    <Compile Include="NotificationReceiveEventArgs.cs" />
 | 
				
			||||||
 | 
					    <Compile Include="NtfyEvent.cs" />
 | 
				
			||||||
    <Compile Include="Program.cs" />
 | 
					    <Compile Include="Program.cs" />
 | 
				
			||||||
    <Compile Include="Properties\AssemblyInfo.cs" />
 | 
					    <Compile Include="Properties\AssemblyInfo.cs" />
 | 
				
			||||||
    <Compile Include="SubscribeDialog.cs">
 | 
					    <Compile Include="SubscribeDialog.cs">
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user