├── art
├── settings.png
├── toolwindow.png
├── view-menu.png
├── context-menu.png
└── full-screen.png
├── src
└── DeveloperNews
│ ├── Resources
│ ├── Icon.png
│ ├── Text.resx
│ └── Text.Designer.cs
│ ├── Settings
│ ├── DialogPageProvider.cs
│ ├── Options.cs
│ ├── BaseOptionPage.cs
│ └── BaseOptionModel.cs
│ ├── Feeds
│ ├── FeedInfo.cs
│ ├── FeedDownloader.cs
│ ├── FeedStore.cs
│ └── FeedOrchestrator.cs
│ ├── Properties
│ └── AssemblyInfo.cs
│ ├── ToolWindows
│ ├── NewsWindow.cs
│ ├── NewsItemTemplateSelector.cs
│ ├── PostControl.xaml.cs
│ ├── SettingsControl.xaml
│ ├── SettingsControl.xaml.cs
│ ├── NewsWindowControl.xaml.cs
│ ├── PostViewModel.cs
│ ├── NewsViewModel.cs
│ ├── VsTheme.cs
│ ├── PostControl.xaml
│ └── NewsWindowControl.xaml
│ ├── source.extension.cs
│ ├── VSPackage.cs
│ ├── VSPackage.vsct
│ ├── Commands
│ └── NewsWindowCommand.cs
│ ├── source.extension.vsixmanifest
│ ├── registry.pkgdef
│ ├── StatusBarInjector.cs
│ ├── DeveloperNewsPackage.cs
│ └── DeveloperNews.csproj
├── test
├── .editorconfig
└── DeveloperNews.Test
│ ├── Properties
│ └── AssemblyInfo.cs
│ ├── DeveloperNews.Test.csproj
│ ├── FeedDownloaderTest.cs
│ └── FeedOrchestratorTest.cs
├── appveyor.yml
├── README.md
├── DeveloperNews.sln
├── .gitattributes
├── .editorconfig
├── .gitignore
└── LICENSE
/art/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madskristensen/DeveloperNews/master/art/settings.png
--------------------------------------------------------------------------------
/art/toolwindow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madskristensen/DeveloperNews/master/art/toolwindow.png
--------------------------------------------------------------------------------
/art/view-menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madskristensen/DeveloperNews/master/art/view-menu.png
--------------------------------------------------------------------------------
/art/context-menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madskristensen/DeveloperNews/master/art/context-menu.png
--------------------------------------------------------------------------------
/art/full-screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madskristensen/DeveloperNews/master/art/full-screen.png
--------------------------------------------------------------------------------
/src/DeveloperNews/Resources/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/madskristensen/DeveloperNews/master/src/DeveloperNews/Resources/Icon.png
--------------------------------------------------------------------------------
/test/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome:http://EditorConfig.org
2 |
3 | [*.{cs,vb}]
4 | # Naming rules - async methods to be prefixed with Async
5 | dotnet_naming_rule.async_methods_must_end_with_async.severity = none
--------------------------------------------------------------------------------
/src/DeveloperNews/Settings/DialogPageProvider.cs:
--------------------------------------------------------------------------------
1 | namespace DevNews
2 | {
3 | ///
4 | /// A provider for custom implementations.
5 | ///
6 | internal class DialogPageProvider
7 | {
8 | public class General : BaseOptionPage { }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/DeveloperNews/Settings/Options.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 |
4 | namespace DevNews
5 | {
6 | ///
7 | /// The options used by this extension.
8 | ///
9 | internal class Options : BaseOptionModel
10 | {
11 | [DefaultValue("")]
12 | [Browsable(false)]
13 | public string FeedSelection { get; set; } = "";
14 |
15 | [Browsable(false)]
16 | public DateTime LastRead { get; set; }
17 |
18 | [Browsable(false)]
19 | public int UnreadPosts { get; set; }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/DeveloperNews/Feeds/FeedInfo.cs:
--------------------------------------------------------------------------------
1 | namespace DevNews
2 | {
3 | ///
4 | /// Represents all the meta data about a news feed.
5 | ///
6 | public class FeedInfo
7 | {
8 | public string Name { get; set; }
9 | public string Url { get; set; }
10 | public bool IsSelected { get; set; }
11 |
12 | public string DisplayName
13 | {
14 | get { return Name.TrimStart('!', '?'); }
15 | }
16 |
17 | public override string ToString()
18 | {
19 | return $"{Name}:{IsSelected}";
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/DeveloperNews/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.InteropServices;
3 | using DevNews;
4 |
5 | [assembly: AssemblyTitle(Vsix.Name)]
6 | [assembly: AssemblyDescription(Vsix.Description)]
7 | [assembly: AssemblyConfiguration("")]
8 | [assembly: AssemblyCompany(Vsix.Author)]
9 | [assembly: AssemblyProduct(Vsix.Name)]
10 | [assembly: AssemblyCopyright(Vsix.Author)]
11 | [assembly: AssemblyTrademark("")]
12 | [assembly: AssemblyCulture("")]
13 |
14 | [assembly: ComVisible(false)]
15 |
16 | [assembly: AssemblyVersion(Vsix.Version)]
17 | [assembly: AssemblyFileVersion(Vsix.Version)]
18 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | image: Visual Studio 2022
2 |
3 | install:
4 | - ps: (new-object Net.WebClient).DownloadString("https://raw.github.com/madskristensen/ExtensionScripts/master/AppVeyor/vsix.ps1") | iex
5 |
6 | before_build:
7 | - ps: Vsix-IncrementVsixVersion | Vsix-UpdateBuildVersion
8 | - ps: Vsix-TokenReplacement src\DeveloperNews\source.extension.cs 'Version = "([0-9\\.]+)"' 'Version = "{version}"'
9 |
10 | build_script:
11 | - nuget restore -Verbosity quiet
12 | - msbuild /p:configuration=Release /p:DeployExtension=false /p:ZipPackageCompressionLevel=normal /v:m
13 |
14 | #test_script:
15 | # - dotnet test test/FeedManager.Test/FeedManager.Test.csproj
16 |
17 | after_test:
18 | - ps: Vsix-PushArtifacts | Vsix-PublishToGallery
19 |
--------------------------------------------------------------------------------
/test/DeveloperNews.Test/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | [assembly: AssemblyTitle("DeveloperNews.Test")]
6 | [assembly: AssemblyDescription("")]
7 | [assembly: AssemblyConfiguration("")]
8 | [assembly: AssemblyCompany("")]
9 | [assembly: AssemblyProduct("DeveloperNews.Test")]
10 | [assembly: AssemblyCopyright("Copyright © 2020")]
11 | [assembly: AssemblyTrademark("")]
12 | [assembly: AssemblyCulture("")]
13 |
14 | [assembly: ComVisible(false)]
15 |
16 | [assembly: Guid("3048d43a-f7cb-4e0d-a91d-1be51eb8f32b")]
17 |
18 | // [assembly: AssemblyVersion("1.0.*")]
19 | [assembly: AssemblyVersion("1.0.0.0")]
20 | [assembly: AssemblyFileVersion("1.0.0.0")]
21 |
--------------------------------------------------------------------------------
/src/DeveloperNews/ToolWindows/NewsWindow.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.InteropServices;
3 | using System.ServiceModel.Syndication;
4 | using DevNews.Resources;
5 | using Microsoft.VisualStudio.Imaging;
6 | using Microsoft.VisualStudio.Shell;
7 |
8 | namespace DevNews.ToolWindows
9 | {
10 | ///
11 | /// The tool window hosting the Developer News content.
12 | ///
13 | [Guid(PackageGuids.guidToolWindowString)]
14 | public class NewsWindow : ToolWindowPane
15 | {
16 | public NewsWindow(SyndicationFeed feed) : base(null)
17 | {
18 | BitmapImageMoniker = KnownMonikers.Dictionary;
19 | Caption = Text.WindowTitle;
20 | Content = new NewsWindowControl(feed);
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/DeveloperNews/ToolWindows/NewsItemTemplateSelector.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 | using System.Windows.Controls;
3 |
4 | namespace DevNews.ToolWindows
5 | {
6 | ///
7 | /// Template selector for news items to distinguish between timeline headers and posts
8 | ///
9 | public class NewsItemTemplateSelector : DataTemplateSelector
10 | {
11 | public DataTemplate TimelineHeaderTemplate { get; set; }
12 | public DataTemplate PostTemplate { get; set; }
13 |
14 | public override DataTemplate SelectTemplate(object item, DependencyObject container)
15 | {
16 | if (item is TimelineHeaderViewModel)
17 | return TimelineHeaderTemplate;
18 |
19 | if (item is PostViewModel)
20 | return PostTemplate;
21 |
22 | return base.SelectTemplate(item, container);
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/src/DeveloperNews/source.extension.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // This file was generated by VSIX Synchronizer
4 | //
5 | // ------------------------------------------------------------------------------
6 | namespace DevNews
7 | {
8 | internal sealed partial class Vsix
9 | {
10 | public const string Id = "36cfa8d9-bd14-4d32-a8a6-34133aa2309e";
11 | public const string Name = "Developer News 2022";
12 | public const string Description = @"Always stay up to date with developer news from the Visual Studio team and other sources right within Visual Studio or your default browser.";
13 | public const string Language = "en-US";
14 | public const string Version = "1.0.900";
15 | public const string Author = "Mads Kristensen";
16 | public const string Tags = "news, rss, feed";
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/DeveloperNews/Settings/BaseOptionPage.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.VisualStudio.Shell;
2 |
3 | namespace DevNews
4 | {
5 | ///
6 | /// A base class for a DialogPage to show in Tools -> Options.
7 | ///
8 | internal class BaseOptionPage : DialogPage where T : BaseOptionModel, new()
9 | {
10 | private readonly BaseOptionModel _model;
11 |
12 | public BaseOptionPage()
13 | {
14 | #pragma warning disable VSTHRD104 // Offer async methods
15 | _model = ThreadHelper.JoinableTaskFactory.Run(BaseOptionModel.CreateAsync);
16 | #pragma warning restore VSTHRD104 // Offer async methods
17 | }
18 |
19 | public override object AutomationObject => _model;
20 |
21 | public override void LoadSettingsFromStorage()
22 | {
23 | _model.Load();
24 | }
25 |
26 | public override void SaveSettingsToStorage()
27 | {
28 | _model.Save();
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/DeveloperNews/ToolWindows/PostControl.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Windows;
3 | using System.Windows.Controls;
4 |
5 | namespace DevNews.ToolWindows
6 | {
7 | public partial class PostControl : UserControl
8 | {
9 | public PostControl()
10 | {
11 | InitializeComponent();
12 | }
13 |
14 | private PostViewModel ViewModel => DataContext as PostViewModel;
15 |
16 | private void PostClick(object sender, RoutedEventArgs e)
17 | {
18 | OpenInDefaultBrowserClick(this, null);
19 | }
20 |
21 | private void OpenInDefaultBrowserClick(object sender, RoutedEventArgs e)
22 | {
23 | if (!string.IsNullOrEmpty(ViewModel?.Url))
24 | {
25 | Process.Start(ViewModel.Url);
26 | }
27 | }
28 |
29 | private void CopyUrlClick(object sender, RoutedEventArgs e)
30 | {
31 | if (!string.IsNullOrEmpty(ViewModel?.Url))
32 | {
33 | Clipboard.SetText(ViewModel.Url);
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/DeveloperNews/ToolWindows/SettingsControl.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/DeveloperNews/VSPackage.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // This file was generated by VSIX Synchronizer
4 | //
5 | // ------------------------------------------------------------------------------
6 | namespace DevNews
7 | {
8 | using System;
9 |
10 | ///
11 | /// Helper class that exposes all GUIDs used across VS Package.
12 | ///
13 | internal sealed partial class PackageGuids
14 | {
15 | public const string guidDeveloperNewsPackageString = "cf0f01e7-96a3-4245-a561-76353cddf32d";
16 | public static Guid guidDeveloperNewsPackage = new Guid(guidDeveloperNewsPackageString);
17 |
18 | public const string guidToolWindowString = "7b3eb750-0ca7-4a08-b0a7-b48c654b741c";
19 | public static Guid guidToolWindow = new Guid(guidToolWindowString);
20 |
21 | public const string guidDeveloperNewsPackageCmdSetString = "257fd01e-353b-43a2-a48a-48fccf3c536f";
22 | public static Guid guidDeveloperNewsPackageCmdSet = new Guid(guidDeveloperNewsPackageCmdSetString);
23 | }
24 | ///
25 | /// Helper class that encapsulates all CommandIDs uses across VS Package.
26 | ///
27 | internal sealed partial class PackageIds
28 | {
29 | public const int NewsWindowCommandId = 0x0100;
30 | }
31 | }
--------------------------------------------------------------------------------
/src/DeveloperNews/VSPackage.vsct:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/DeveloperNews/Commands/NewsWindowCommand.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.Design;
2 | using DevNews.ToolWindows;
3 | using Microsoft;
4 | using Microsoft.VisualStudio.Shell;
5 | using Task = System.Threading.Tasks.Task;
6 |
7 | namespace DevNews
8 | {
9 | ///
10 | /// Handles the command invocation to show the News tool window
11 | ///
12 | internal sealed class NewsWindowCommand
13 | {
14 | ///
15 | /// Hooks up the "View -> Developer News" command and assigns execution handler.
16 | ///
17 | public static async Task InitializeAsync(AsyncPackage package)
18 | {
19 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken);
20 |
21 | var commandService = await package.GetServiceAsync((typeof(IMenuCommandService))) as OleMenuCommandService;
22 | Assumes.Present(commandService);
23 |
24 | var cmdId = new CommandID(PackageGuids.guidDeveloperNewsPackageCmdSet, PackageIds.NewsWindowCommandId);
25 | var cmd = new MenuCommand((s, e) => Execute(package), cmdId);
26 | commandService.AddCommand(cmd);
27 | }
28 |
29 | ///
30 | /// Opens the "News" tool window in an asynchronous way.
31 | ///
32 | private static void Execute(AsyncPackage package)
33 | {
34 | package.JoinableTaskFactory.RunAsync(async delegate
35 | {
36 | ToolWindowPane window = await package.ShowToolWindowAsync(typeof(NewsWindow), 0, true, package.DisposalToken);
37 | Assumes.Present(window);
38 | }).FileAndForget(nameof(NewsWindowCommand));
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Developer News in Visual Studio
2 |
3 | [](https://ci.appveyor.com/project/madskristensen/developernews)
4 |
5 | Always stay up to date with developer news from the Visual Studio team and other sources right within Visual Studio or your default browser.
6 |
7 | Download this extension from the [Marketplace](https://marketplace.visualstudio.com/items?itemName=MadsKristensen.DeveloperNews)
8 | or get the [CI build](https://www.vsixgallery.com/extension/36cfa8d9-bd14-4d32-a8a6-34133aa2309d/).
9 |
10 | ----------------------------------------------
11 |
12 | **Help ensure this will be built in to Visual Studio** by voting on [Bring back the 'Developer News'](https://developercommunity.visualstudio.com/idea/399833/bring-back-the-developer-news-on-startup.html) suggestion.
13 |
14 | Get the daily news about Visual Studio and other relevant topics right inside Visual Studio.
15 |
16 | It's simple, it's easy, and it keeps you up-to-date.
17 |
18 | 
19 |
20 | ## Open the tool window
21 | Open the *News* tool window from the *View* top level menu.
22 |
23 | 
24 |
25 | You can also use the keyboard shortcut **Control + Alt + N**.
26 |
27 | ## Design
28 | The *News* tool window looks great in any theme and looks like a built-in part of Visual Studio.
29 |
30 | 
31 |
32 | ## Context menu
33 | Right-clicking on any of the posts will show a context menu with various options.
34 |
35 | 
36 |
37 | ## Settings
38 | Decide if you want the news to open inside Visual Studio or in your default browser. And select the news feeds that are relevant to you.
39 |
40 | 
41 |
42 | ## License
43 | [Apache 2.0](LICENSE)
44 |
--------------------------------------------------------------------------------
/src/DeveloperNews/source.extension.vsixmanifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Developer News 2022
6 | Always stay up to date with developer news from the Visual Studio team and other sources right within Visual Studio or your default browser.
7 | https://github.com/madskristensen/DeveloperNews
8 | Resources\LICENSE
9 | Resources\Icon.png
10 | Resources\Icon.png
11 | news, rss, feed
12 |
13 |
14 |
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/DeveloperNews/registry.pkgdef:
--------------------------------------------------------------------------------
1 | ; List of feeds to include
2 | ; '!' in front of a name, makes it selected and mandatory
3 | ; '?' in front of a name makes the feed deselected by default
4 | [$RootKey$\DeveloperNews\Feeds]
5 | "!Visual Studio"="https://devblogs.microsoft.com/visualstudio/feed/"
6 | "?.NET"="https://devblogs.microsoft.com/dotnet/feed/"
7 | "?Azure AI Foundry"="https://devblogs.microsoft.com/foundry/feed/"
8 | "?Azure Blog"="https://azurecomcdn.azureedge.net/en-us/blog/feed/"
9 | "?Azure DevOps"="https://devblogs.microsoft.com/devops/feed/"
10 | "?Azure SDKs"="https://devblogs.microsoft.com/azure-sdk/feed/"
11 | "?C++"="https://devblogs.microsoft.com/cppblog/feed/"
12 | "?Command Line"="https://devblogs.microsoft.com/commandline/feed/"
13 | "?CosmosDB"="https://devblogs.microsoft.com/cosmosdb/feed/"
14 | "?DirectX"="https://devblogs.microsoft.com/directx/feed/"
15 | "?IoT"="https://devblogs.microsoft.com/iotdev/feed/"
16 | "?Microsoft 365"="https://devblogs.microsoft.com/microsoft365dev/feed/"
17 | "?Microsoft Blog"="https://blogs.microsoft.com/feed/"
18 | "?Microsoft Education"="https://www.microsoft.com/en-us/education/blog/feed/"
19 | "?Microsoft News"="https://news.microsoft.com/feed/"
20 | "?Microsoft News"="https://news.microsoft.com/feed/"
21 | "?NuGet"="https://devblogs.microsoft.com/nuget/feed/"
22 | "?PowerShell"="https://devblogs.microsoft.com/powershell/feed/"
23 | "?Python"="https://devblogs.microsoft.com/python/feed/"
24 | "?SQL Server"="https://cloudblogs.microsoft.com/sqlserver/feed/"
25 | "?TypeScript"="https://devblogs.microsoft.com/typescript/feed/"
26 | ;"?VS Office Hours"="https://www.youtube.com/feeds/videos.xml?playlist_id=PLReL099Y5nRem6UA4w849hTfd0LzBIsAk"
27 | ;"VS Tips & Tricks"="https://www.youtube.com/feeds/videos.xml?playlist_id=PLReL099Y5nRc-zbaFbf0aNcIamBQujOxP"
28 | "?Visual Studio Code"="https://code.visualstudio.com/feed.xml"
29 | "?Windows Developer"="https://blogs.windows.com/windowsdeveloper/feed/"
--------------------------------------------------------------------------------
/DeveloperNews.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30216.92
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeveloperNews", "src\DeveloperNews\DeveloperNews.csproj", "{F55B991E-300E-4CEF-BABA-8636F1F08C9B}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B2A9E962-0741-4578-82AA-2F2990B4A4B2}"
9 | ProjectSection(SolutionItems) = preProject
10 | .editorconfig = .editorconfig
11 | appveyor.yml = appveyor.yml
12 | README.md = README.md
13 | EndProjectSection
14 | EndProject
15 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeveloperNews.Test", "Test\DeveloperNews.Test\DeveloperNews.Test.csproj", "{3048D43A-F7CB-4E0D-A91D-1BE51EB8F32B}"
16 | EndProject
17 | Global
18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
19 | Debug|Any CPU = Debug|Any CPU
20 | Release|Any CPU = Release|Any CPU
21 | EndGlobalSection
22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
23 | {F55B991E-300E-4CEF-BABA-8636F1F08C9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24 | {F55B991E-300E-4CEF-BABA-8636F1F08C9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
25 | {F55B991E-300E-4CEF-BABA-8636F1F08C9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
26 | {F55B991E-300E-4CEF-BABA-8636F1F08C9B}.Release|Any CPU.Build.0 = Release|Any CPU
27 | {3048D43A-F7CB-4E0D-A91D-1BE51EB8F32B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28 | {3048D43A-F7CB-4E0D-A91D-1BE51EB8F32B}.Debug|Any CPU.Build.0 = Debug|Any CPU
29 | {3048D43A-F7CB-4E0D-A91D-1BE51EB8F32B}.Release|Any CPU.ActiveCfg = Release|Any CPU
30 | {3048D43A-F7CB-4E0D-A91D-1BE51EB8F32B}.Release|Any CPU.Build.0 = Release|Any CPU
31 | EndGlobalSection
32 | GlobalSection(SolutionProperties) = preSolution
33 | HideSolutionNode = FALSE
34 | EndGlobalSection
35 | GlobalSection(ExtensibilityGlobals) = postSolution
36 | SolutionGuid = {EB4884F5-208C-4EFF-AF05-512BA7FD7BF7}
37 | EndGlobalSection
38 | EndGlobal
39 |
--------------------------------------------------------------------------------
/src/DeveloperNews/ToolWindows/SettingsControl.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Windows;
5 | using System.Windows.Controls;
6 |
7 | namespace DevNews.ToolWindows
8 | {
9 | public partial class SettingsControl : UserControl
10 | {
11 | public SettingsControl()
12 | {
13 | InitializeComponent();
14 | }
15 |
16 | public void BindSettings()
17 | {
18 | pnlFeedSelection.Children.Clear();
19 |
20 | foreach (FeedInfo feedInfo in DeveloperNewsPackage.Store.FeedInfos.OrderBy(f => f.Name.TrimStart('?')))
21 | {
22 | var cb = new CheckBox
23 | {
24 | Content = feedInfo.DisplayName,
25 | Padding = new Thickness(5, 2, 0, 2),
26 | IsChecked = feedInfo.IsSelected,
27 | Tag = feedInfo,
28 | IsEnabled = feedInfo.Name[0] != '!'
29 | };
30 |
31 | pnlFeedSelection.Children.Add(cb);
32 | }
33 | }
34 |
35 | private void SaveClick(object sender, RoutedEventArgs e)
36 | {
37 | var feedInfos = new List();
38 |
39 | foreach (CheckBox cb in pnlFeedSelection.Children.Cast())
40 | {
41 | var feedInfo = cb.Tag as FeedInfo;
42 |
43 | feedInfo.IsSelected = cb.IsChecked.Value;
44 | feedInfos.Add(feedInfo);
45 | }
46 |
47 | // Set selected feeds
48 | DeveloperNewsPackage.Store.FeedInfos = feedInfos;
49 |
50 | // Save selection and Options
51 | DeveloperNewsPackage.Store.SaveSelection();
52 |
53 | CancelClick(sender, e);
54 |
55 | Saved?.Invoke(this, EventArgs.Empty);
56 | }
57 |
58 | private void CancelClick(object sender, RoutedEventArgs e)
59 | {
60 | Visibility = Visibility.Collapsed;
61 | }
62 |
63 | public event EventHandler Saved;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/DeveloperNews/StatusBarInjector.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using System.Windows;
3 | using System.Windows.Controls;
4 | using System.Windows.Media;
5 | using Microsoft.VisualStudio.Shell;
6 |
7 | namespace DevNews
8 | {
9 | internal static class StatusBarInjector
10 | {
11 | private static Panel _panel;
12 |
13 | // Constants for better maintainability
14 | private const string StatusBarPanelName = "StatusBarPanel";
15 | private const int StatusBarRetryDelayMilliseconds = 5000;
16 |
17 | private static DependencyObject FindChild(DependencyObject parent, string childName)
18 | {
19 | if (parent == null)
20 | {
21 | return null;
22 | }
23 |
24 | int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
25 |
26 | for (int i = 0; i < childrenCount; i++)
27 | {
28 | DependencyObject child = VisualTreeHelper.GetChild(parent, i);
29 |
30 | if (child is FrameworkElement frameworkElement && frameworkElement.Name == childName)
31 | {
32 | return frameworkElement;
33 | }
34 |
35 | child = FindChild(child, childName);
36 |
37 | if (child != null)
38 | {
39 | return child;
40 | }
41 | }
42 |
43 | return null;
44 | }
45 |
46 | private static async Task EnsureUIAsync()
47 | {
48 | while (_panel is null)
49 | {
50 | _panel = FindChild(Application.Current?.MainWindow, StatusBarPanelName) as DockPanel;
51 | if (_panel is null)
52 | {
53 | // Start window is showing. Need to wait for status bar render.
54 | await Task.Delay(StatusBarRetryDelayMilliseconds);
55 | }
56 | }
57 | }
58 |
59 | public static async Task InjectControlAsync(FrameworkElement element)
60 | {
61 | if (element == null) return;
62 |
63 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
64 | await EnsureUIAsync();
65 |
66 | element.SetValue(DockPanel.DockProperty, Dock.Right);
67 | _panel?.Children.Insert(1, element);
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/src/DeveloperNews/ToolWindows/NewsWindowControl.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ServiceModel.Syndication;
3 | using System.Windows;
4 | using System.Windows.Controls;
5 | using Microsoft.VisualStudio.Shell;
6 | using Microsoft.VisualStudio.Threading;
7 |
8 | namespace DevNews.ToolWindows
9 | {
10 | public partial class NewsWindowControl : UserControl
11 | {
12 | private readonly NewsViewModel _viewModel;
13 |
14 | public NewsWindowControl(SyndicationFeed feed)
15 | {
16 | InitializeComponent();
17 |
18 | _viewModel = new NewsViewModel();
19 | DataContext = _viewModel;
20 |
21 | BindPosts(feed);
22 | RefreshClick(this, null);
23 | }
24 |
25 | private void SettingsSaved(object sender, EventArgs e)
26 | {
27 | RefreshClick(this, null);
28 | }
29 |
30 | private void BindPosts(SyndicationFeed feed)
31 | {
32 | _viewModel.UpdateFeed(feed);
33 |
34 | _ = ThreadHelper.JoinableTaskFactory.StartOnIdle(async () =>
35 | {
36 | Options options = await Options.GetLiveInstanceAsync();
37 | options.LastRead = DateTime.Now;
38 | options.UnreadPosts = 0;
39 | await options.SaveAsync();
40 | });
41 | }
42 |
43 | private void RefreshClick(object sender, RoutedEventArgs e)
44 | {
45 | prsLoader.Visibility = Visibility.Visible;
46 | lnkRefresh.IsEnabled = false;
47 | scrollViewer.IsEnabled = false;
48 |
49 | ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
50 | {
51 | try
52 | {
53 | SyndicationFeed feed = await DeveloperNewsPackage.Store.GetFeedAsync(true);
54 |
55 | await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
56 | BindPosts(feed);
57 | }
58 | finally
59 | {
60 | prsLoader.Visibility = Visibility.Hidden;
61 | lnkRefresh.IsEnabled = true;
62 | scrollViewer.IsEnabled = true;
63 | }
64 | }).Task.Forget();
65 | }
66 |
67 | private void OpenSettings(object sender, RoutedEventArgs e)
68 | {
69 | ctrlSettings.Visibility = ctrlSettings.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
70 |
71 | if (ctrlSettings.Visibility == Visibility.Visible)
72 | {
73 | ctrlSettings.BindSettings();
74 | }
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/src/DeveloperNews/ToolWindows/PostViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 | using System.Linq;
4 | using System.Net;
5 | using System.ServiceModel.Syndication;
6 | using System.Text.RegularExpressions;
7 | using DevNews.Resources;
8 |
9 | namespace DevNews.ToolWindows
10 | {
11 | ///
12 | /// View model for a news post item
13 | ///
14 | public class PostViewModel : INotifyPropertyChanged
15 | {
16 | private static readonly Regex _htmlRegex = new Regex(@"?\w+((\s+\w+(\s*=\s*(?:"".*?""|'.*?'|[^'"">\s]+))?)+\s*|\s*)/?>",
17 | RegexOptions.Singleline | RegexOptions.Compiled);
18 |
19 | // Constants for better maintainability
20 | private const string TrackingParameter = "?cid=vs_developer_news";
21 | private const int MaxSummaryLength = 1000;
22 |
23 | public PostViewModel(SyndicationItem item)
24 | {
25 | if (item == null) return;
26 |
27 | Title = WebUtility.HtmlDecode(item.Title?.Text ?? "").Trim();
28 | Url = item.Links?.FirstOrDefault()?.Uri?.OriginalString ?? "";
29 |
30 | // Add tracking parameter if not present
31 | if (!string.IsNullOrEmpty(Url) && !Url.Contains('?'))
32 | {
33 | Url += TrackingParameter;
34 | }
35 |
36 | var summary = item.Summary?.Text ?? Text.NoDescription;
37 | Summary = WebUtility.HtmlDecode(TruncateHtml(summary.Trim()));
38 |
39 | PublishDate = item.PublishDate.DateTime;
40 | Source = $"{item.PublishDate:MMM d} in {item.SourceFeed?.Title?.Text}";
41 |
42 | ToolTip = $"{Title}\r\n{item.PublishDate:MMMM d, yyyy}";
43 |
44 | HasDescription = Summary != Text.NoDescription;
45 | }
46 |
47 | public string Title { get; }
48 | public string Summary { get; }
49 | public string Url { get; }
50 | public DateTime PublishDate { get; }
51 | public string Source { get; }
52 | public string ToolTip { get; }
53 | public bool HasDescription { get; }
54 |
55 | private static string TruncateHtml(string input)
56 | {
57 | var clearText = _htmlRegex.Replace(input, "").Replace("\r", " ").Replace("\n", "");
58 | var maxLength = Math.Min(MaxSummaryLength, clearText.Length);
59 | return clearText.Substring(0, maxLength);
60 | }
61 |
62 | public event PropertyChangedEventHandler PropertyChanged;
63 | }
64 |
65 | ///
66 | /// View model for a timeline header
67 | ///
68 | public class TimelineHeaderViewModel
69 | {
70 | public TimelineHeaderViewModel(string header)
71 | {
72 | Header = header;
73 | }
74 |
75 | public string Header { get; }
76 | }
77 | }
--------------------------------------------------------------------------------
/src/DeveloperNews/ToolWindows/NewsViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.ObjectModel;
3 | using System.ComponentModel;
4 | using System.Linq;
5 | using System.ServiceModel.Syndication;
6 | using DevNews.Resources;
7 |
8 | namespace DevNews.ToolWindows
9 | {
10 | ///
11 | /// View model for the news window
12 | ///
13 | public class NewsViewModel : INotifyPropertyChanged
14 | {
15 | private string _totalCount;
16 | private ObservableCollection